├── back ├── tools │ ├── .gitignore │ ├── iso │ │ ├── file │ │ │ ├── openbdb │ │ │ │ └── Dockerfile │ │ │ └── openbsrv-full │ │ │ │ └── Dockerfile │ │ ├── compose │ │ │ └── poc │ │ │ │ └── docker-compose.yml │ │ └── dev │ ├── install-tools │ ├── install-go │ └── install-mariadb ├── internal │ ├── usersvc │ │ ├── internal │ │ │ ├── asset │ │ │ │ ├── assets │ │ │ │ │ ├── 0002_setup_role_table.sql │ │ │ │ │ ├── 0003_setup_join_user_role_join_table.sql │ │ │ │ │ └── 0001_setup_user_table.sql │ │ │ │ └── asset.go │ │ │ └── userdb │ │ │ │ ├── userdb.go │ │ │ │ └── qryuser.go │ │ └── usersvc.go │ ├── postsvc │ │ ├── internal │ │ │ ├── asset │ │ │ │ ├── assets │ │ │ │ │ ├── 0003_add_post_fulltext_cols.sql │ │ │ │ │ ├── 0002_setup_type_table.sql │ │ │ │ │ └── 0001_setup_post_table.sql │ │ │ │ └── asset.go │ │ │ └── postdb │ │ │ │ ├── postdb.go │ │ │ │ └── qrypost.go │ │ └── postsvc.go │ ├── httpsrv │ │ ├── internal │ │ │ └── asset │ │ │ │ └── asset.go │ │ ├── gogen.go │ │ └── httpsrv.go │ ├── pb │ │ ├── gogen.go │ │ ├── auth_grpc.pb.go │ │ ├── post_grpc.pb.go │ │ └── user_grpc.pb.go │ ├── grpcsrv │ │ ├── io.go │ │ └── grpcsrv.go │ ├── altr │ │ └── altr.go │ ├── go.mod │ ├── log │ │ └── log.go │ ├── authsvc │ │ └── authsvc.go │ └── dbg │ │ ├── dbg.go │ │ └── dbg_test.go ├── gengo ├── tests │ └── openbsrv │ │ ├── go.mod │ │ ├── common_test.go │ │ ├── run-tests │ │ ├── usersvc_test.go │ │ └── postsvc_test.go ├── cmd │ └── openbsrv │ │ ├── go.mod │ │ ├── README.md │ │ ├── httpsrv.go │ │ ├── srvmgmt.go │ │ ├── frontsrv.go │ │ ├── grpcsrv.go │ │ ├── db.go │ │ └── main.go └── README.md ├── front ├── .gitignore ├── public │ ├── favicon.ico │ ├── conf │ │ └── manifest.json │ ├── boot.js │ └── index.html ├── src │ ├── Viewer.elm │ ├── Session.elm │ ├── Page.elm │ ├── Page │ │ ├── Login.elm │ │ ├── Home.elm │ │ └── Posts.elm │ ├── Route.elm │ ├── Proto │ │ └── Auth.elm │ ├── Ui.elm │ └── Main.elm ├── genelm ├── tests │ └── Tests.elm ├── elm.json ├── README.md └── bin │ └── pb2elm ├── docs ├── learning │ ├── README.md │ ├── go_cmd.md │ └── git.md ├── AUTHORS ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── projmgmt-issue-set_claimed.yml │ ├── projmgmt-pullreq-set_initiated.yml │ ├── projmgmt-pullreq-set_submitted.yml │ ├── projmgmt-pullreq-set_unsubmitted.yml │ └── wip.yml ├── msgs └── proto │ ├── include │ └── googleapis │ │ ├── README.grpc-gateway │ │ ├── google │ │ ├── api │ │ │ └── annotations.proto │ │ └── rpc │ │ │ ├── status.proto │ │ │ ├── code.proto │ │ │ └── error_details.proto │ │ └── LICENSE │ ├── auth.proto │ ├── post.proto │ └── user.proto ├── README.md └── LICENSE /back/tools/.gitignore: -------------------------------------------------------------------------------- 1 | .bin 2 | .include 3 | -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | public/app.js 2 | elm-stuff/ -------------------------------------------------------------------------------- /front/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenEugene/openboard/HEAD/front/public/favicon.ico -------------------------------------------------------------------------------- /docs/learning/README.md: -------------------------------------------------------------------------------- 1 | # Walk-Throughs 2 | 3 | - [Git](./git.md) 4 | 5 | ## go build 6 | 7 | TODO: this 8 | 9 | ## go test 10 | 11 | TODO: this 12 | -------------------------------------------------------------------------------- /front/src/Viewer.elm: -------------------------------------------------------------------------------- 1 | module Viewer exposing (Viewer(..), unbox) 2 | 3 | 4 | type Viewer 5 | = Viewer String 6 | 7 | 8 | unbox : Viewer -> String 9 | unbox (Viewer v) = 10 | v 11 | -------------------------------------------------------------------------------- /docs/AUTHORS: -------------------------------------------------------------------------------- 1 | Jason Valenzuela (@toyotathon) 2 | Daved (@daved) 3 | Nathan Nichols (@nqthqn) 4 | Greg McKelvey (@mckelveygreg) 5 | F. Gold (@codegold79) 6 | Daniel Coats (@coatsd) 7 | Alex Anderson (@appins) 8 | Anna G. (@annag42) 9 | -------------------------------------------------------------------------------- /back/internal/usersvc/internal/asset/assets/0002_setup_role_table.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | CREATE TABLE role ( 4 | role_id VARCHAR(26) NOT NULL PRIMARY KEY, 5 | role_name VARCHAR(255) NOT NULL UNIQUE 6 | ); 7 | 8 | -- +migrate Down 9 | 10 | DROP TABLE IF EXISTS role; 11 | -------------------------------------------------------------------------------- /back/internal/postsvc/internal/asset/assets/0003_add_post_fulltext_cols.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE post ADD FULLTEXT(body); 4 | ALTER TABLE post ADD FULLTEXT(title); 5 | 6 | -- +migrate Down 7 | 8 | ALTER TABLE post DROP INDEX IF EXISTS body; 9 | ALTER TABLE post DROP INDEX IF EXISTS title; 10 | -------------------------------------------------------------------------------- /back/internal/usersvc/internal/asset/assets/0003_setup_join_user_role_join_table.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | CREATE TABLE user_role ( 4 | user_id VARCHAR(26) NOT NULL, 5 | role_id VARCHAR(26) NOT NULL, 6 | PRIMARY KEY (user_id, role_id) 7 | ); 8 | 9 | -- +migrate Down 10 | 11 | DROP TABLE IF EXISTS user_role; 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Create a feature request 4 | title: 'Feature: ' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ## Desired Behavior 10 | 11 | 12 | ## Suggested Solution(s) 13 | 14 | 15 | ## Screenshot(s) If Applicable 16 | 17 | 18 | ## Additional Notes 19 | 20 | -------------------------------------------------------------------------------- /back/gengo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function main() { 4 | local rootDir="$(dirname "${0}")" 5 | local embedDir=".embeds" 6 | local dirs=( 7 | "internal/pb" 8 | "internal/httpsrv" 9 | ) 10 | 11 | for dir in "${dirs[@]}"; do 12 | pushd "${rootDir}/${dir}" &> /dev/null 13 | go generate 14 | popd &> /dev/null 15 | done 16 | } 17 | 18 | main 19 | -------------------------------------------------------------------------------- /front/public/conf/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "openboard", 3 | "name": "openboard", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /back/internal/httpsrv/internal/asset/asset.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io/fs" 7 | ) 8 | 9 | var ( 10 | //go:embed assets/* 11 | FS embed.FS 12 | ) 13 | 14 | func NewFS() (fs.FS, error) { 15 | s, err := fs.Sub(FS, "assets") 16 | if err != nil { 17 | return nil, fmt.Errorf("connect to contents fs: %w", err) 18 | } 19 | 20 | return s, nil 21 | } 22 | -------------------------------------------------------------------------------- /back/internal/postsvc/internal/asset/asset.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io/fs" 7 | ) 8 | 9 | var ( 10 | //go:embed assets/* 11 | FS embed.FS 12 | ) 13 | 14 | func NewFS() (fs.FS, error) { 15 | s, err := fs.Sub(FS, "assets") 16 | if err != nil { 17 | return nil, fmt.Errorf("connect to contents fs: %w", err) 18 | } 19 | 20 | return s, nil 21 | } 22 | -------------------------------------------------------------------------------- /back/internal/usersvc/internal/asset/asset.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io/fs" 7 | ) 8 | 9 | var ( 10 | //go:embed assets/* 11 | FS embed.FS 12 | ) 13 | 14 | func NewFS() (fs.FS, error) { 15 | s, err := fs.Sub(FS, "assets") 16 | if err != nil { 17 | return nil, fmt.Errorf("connect to contents fs: %w", err) 18 | } 19 | 20 | return s, nil 21 | } 22 | -------------------------------------------------------------------------------- /front/genelm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function main() { 4 | local rootDir="$(dirname "${0}")" 5 | local fpb2elm="$(find "${rootDir}" -name pb2elm -print -quit)" 6 | 7 | [[ ! -f "${fpb2elm}" || ! -x "${fpb2elm}" ]] && echo "cannot find pb2elm" >&2 && exit 1 8 | 9 | "${fpb2elm}" "${rootDir}/src" -I "${rootDir}/../msgs" "${rootDir}/../msgs/proto/*.proto" 10 | } 11 | 12 | main 13 | -------------------------------------------------------------------------------- /back/tests/openbsrv/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OpenEugene/openboard/back/tests/openbsrv 2 | 3 | go 1.12 4 | 5 | replace github.com/OpenEugene/openboard/back/internal => ../../internal 6 | 7 | require ( 8 | github.com/OpenEugene/openboard/back/internal v0.0.0-20191127022437-d710cfcd0696 9 | github.com/golang/protobuf v1.4.3 10 | google.golang.org/grpc v1.35.0 11 | google.golang.org/protobuf v1.25.0 12 | ) 13 | -------------------------------------------------------------------------------- /back/internal/httpsrv/gogen.go: -------------------------------------------------------------------------------- 1 | package httpsrv 2 | 3 | // alias protobuf/swagger 4 | //go:generate -command pb-sw protoc -I ../../../msgs/proto -I ../../tools/.include -I ../../../msgs/proto/include/googleapis --openapiv2_out=logtostderr=true,allow_merge=true:internal/asset/assets 5 | 6 | // generate swagger.json 7 | //go:generate pb-sw ../../../msgs/proto/auth.proto ../../../msgs/proto/user.proto ../../../msgs/proto/post.proto 8 | -------------------------------------------------------------------------------- /.github/workflows/projmgmt-issue-set_claimed.yml: -------------------------------------------------------------------------------- 1 | name: Manage issues for project (set claimed) 2 | 3 | on: 4 | issues: 5 | types: [assigned] 6 | 7 | jobs: 8 | automate-project-columns: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: alex-page/github-project-automation-plus@v0.2.4 12 | with: 13 | project: Issue Qualification 14 | column: Claimed 15 | repo-token: ${{ secrets.GH_TOKEN }} 16 | -------------------------------------------------------------------------------- /back/internal/postsvc/internal/asset/assets/0002_setup_type_table.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | CREATE TABLE `type` ( 4 | type_id VARCHAR(26) NOT NULL PRIMARY KEY, 5 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 6 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 7 | deleted_at DATETIME DEFAULT NULL, 8 | 9 | name VARCHAR(255) NOT NULL UNIQUE 10 | ); 11 | 12 | -- +migrate Down 13 | 14 | DROP TABLE IF EXISTS `type`; -------------------------------------------------------------------------------- /.github/workflows/projmgmt-pullreq-set_initiated.yml: -------------------------------------------------------------------------------- 1 | name: Manage pull requests for project (set initiated) 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | 7 | jobs: 8 | automate-project-columns: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: alex-page/github-project-automation-plus@v0.2.4 12 | with: 13 | project: Change Management 14 | column: Initiated 15 | repo-token: ${{ secrets.GH_TOKEN }} 16 | -------------------------------------------------------------------------------- /back/internal/pb/gogen.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | // alias protobuf/grpc, protobuf/grpc-gateway 4 | //go:generate -command gen-pb protoc -I ../../tools/.include -I ../../../msgs/proto -I ../../../msgs/proto/include/googleapis --go_out=. --go-grpc_out=require_unimplemented_servers=false:. --grpc-gateway_out=logtostderr=true:. 5 | 6 | // generate grpc, grpc-gateway 7 | //go:generate gen-pb ../../../msgs/proto/auth.proto ../../../msgs/proto/user.proto ../../../msgs/proto/post.proto 8 | -------------------------------------------------------------------------------- /.github/workflows/projmgmt-pullreq-set_submitted.yml: -------------------------------------------------------------------------------- 1 | name: Manage pull requests for project (set submitted) 2 | 3 | on: 4 | pull_request: 5 | types: [review_requested] 6 | 7 | jobs: 8 | automate-project-columns: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: alex-page/github-project-automation-plus@v0.2.4 12 | with: 13 | project: Change Management 14 | column: Submitted 15 | repo-token: ${{ secrets.GH_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/projmgmt-pullreq-set_unsubmitted.yml: -------------------------------------------------------------------------------- 1 | name: Manage pull requests for project (set unsubmitted) 2 | 3 | on: 4 | pull_request: 5 | types: [review_requested_removed] 6 | 7 | jobs: 8 | automate-project-columns: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: alex-page/github-project-automation-plus@v0.2.4 12 | with: 13 | project: Change Management 14 | column: Initiated 15 | repo-token: ${{ secrets.GH_TOKEN }} 16 | -------------------------------------------------------------------------------- /back/internal/grpcsrv/io.go: -------------------------------------------------------------------------------- 1 | package grpcsrv 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | func tcpListener(port string) (*net.TCPListener, error) { 9 | we := func(err error) error { 10 | return fmt.Errorf("cannot create tcp listener: %s", err) 11 | } 12 | 13 | a, err := net.ResolveTCPAddr("tcp", port) 14 | if err != nil { 15 | return nil, we(err) 16 | } 17 | 18 | l, err := net.ListenTCP("tcp", a) 19 | if err != nil { 20 | return nil, we(err) 21 | } 22 | 23 | return l, nil 24 | } 25 | -------------------------------------------------------------------------------- /back/cmd/openbsrv/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OpenEugene/openboard/back/cmd/openbsrv 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/OpenEugene/openboard/back/internal v0.0.0 7 | github.com/codemodus/alfred v0.2.1 8 | github.com/codemodus/chain/v2 v2.1.2 9 | github.com/codemodus/hedrs v0.1.1 10 | github.com/codemodus/sigmon/v2 v2.0.1 11 | github.com/codemodus/sqlmig v0.2.3 12 | github.com/go-sql-driver/mysql v1.5.0 13 | ) 14 | 15 | replace github.com/OpenEugene/openboard/back/internal => ../../internal 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a bug report 4 | title: 'Bug: ' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ## Expected Behavior 10 | 11 | 12 | ## Actual Behavior 13 | 14 | 15 | ## Steps To Reproduce The Problem 16 | 17 | 1. 18 | 1. 19 | 1. 20 | 21 | ## Screenshot(s) If Applicable 22 | 23 | 24 | ## Specifications 25 | 26 | - Platform: [e.g. iOS 11.4.1, Windows 20H1 (10.0.18975)] 27 | - Browser: [e.g. Chrome 76.0.3, Safari 12.1.1] 28 | 29 | ## Additional Notes 30 | 31 | -------------------------------------------------------------------------------- /back/internal/postsvc/internal/asset/assets/0001_setup_post_table.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | CREATE TABLE post ( 4 | post_id VARCHAR(26) NOT NULL PRIMARY KEY, 5 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 6 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 7 | deleted_at DATETIME DEFAULT NULL, 8 | 9 | type_id VARCHAR(26) NOT NULL, 10 | slug VARCHAR(255) NULL, 11 | title VARCHAR(255) NULL, 12 | body VARCHAR(255) NULL 13 | ); 14 | 15 | -- +migrate Down 16 | 17 | DROP TABLE IF EXISTS post; -------------------------------------------------------------------------------- /msgs/proto/include/googleapis/README.grpc-gateway: -------------------------------------------------------------------------------- 1 | Google APIs 2 | ============ 3 | 4 | Project: Google APIs 5 | URL: https://github.com/google/googleapis 6 | Revision: 3544ab16c3342d790b00764251e348705991ea4b 7 | License: Apache License 2.0 8 | 9 | 10 | Imported Files 11 | --------------- 12 | 13 | - google/api/annotations.proto 14 | - google/api/http.proto 15 | 16 | 17 | Generated Files 18 | ---------------- 19 | 20 | They are generated from the .proto files by protoc-gen-go. 21 | - google/api/annotations.pb.go 22 | - google/api/http.pb.go 23 | -------------------------------------------------------------------------------- /front/tests/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing (..) 2 | 3 | import Test exposing (..) 4 | import Expect 5 | 6 | 7 | -- Check out http://package.elm-lang.org/packages/elm-community/elm-test/latest to learn more about testing in Elm! 8 | 9 | 10 | all : Test 11 | all = 12 | describe "A Test Suite" 13 | [ test "Addition" <| 14 | \_ -> 15 | Expect.equal 10 (3 + 7) 16 | , test "String.left" <| 17 | \_ -> 18 | Expect.equal "a" (String.left 1 "abcdefg") 19 | , test "This test should fail" <| 20 | \_ -> 21 | Expect.fail "failed as expected!" 22 | ] 23 | -------------------------------------------------------------------------------- /back/internal/altr/altr.go: -------------------------------------------------------------------------------- 1 | package altr 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/golang/protobuf/ptypes" 8 | "github.com/golang/protobuf/ptypes/timestamp" 9 | ) 10 | 11 | // CSVFromStrings ... 12 | func CSVFromStrings(ss []string) string { 13 | return strings.Join(ss, ",") 14 | } 15 | 16 | // LimitUint32 ... 17 | func LimitUint32(n uint32) uint32 { 18 | if n == 0 { 19 | return 1<<32 - 1 20 | } 21 | return n 22 | } 23 | 24 | // Timestamp ... 25 | func Timestamp(t time.Time, valid bool) *timestamp.Timestamp { 26 | var ts *timestamp.Timestamp 27 | if valid { 28 | ts, _ = ptypes.TimestampProto(t) 29 | } 30 | return ts 31 | } 32 | -------------------------------------------------------------------------------- /back/tools/iso/file/openbdb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mariadb:10.3 2 | 3 | RUN apt-get update && apt-get dist-upgrade -y 4 | 5 | ENV MYSQL_ROOT_PASSWORD="" 6 | ENV MYSQL_DATABASE="" 7 | ENV MYSQL_USER="" 8 | ENV MYSQL_PASSWORD="" 9 | ENV MYSQL_ALLOW_EMPTY_PASSWORD = no 10 | ENV MYSQL_RANDOM_ROOT_PASSWORD = no 11 | 12 | ARG mycnf="/etc/mysql/my.cnf" 13 | RUN sed -Ei 's/^(.*)(bind.*ress\s*=\s*)[.0-9]{7,15}/\20.0.0.0/g' ${mycnf} 14 | ARG mycnf="/etc/mysql/mariadb.cnf" 15 | RUN sed -Ei 's/^(.*)(char.*rver\s*=\s*)[[:alnum:][:punct:]]*/\2utf8mb4/g' ${mycnf} 16 | RUN sed -Ei 's/^(.*)(coll.*rver\s*=\s*)[[:alnum:][:punct:]]*/\2utf8mb4_unicode_ci/g' ${mycnf} 17 | 18 | EXPOSE 3306 19 | -------------------------------------------------------------------------------- /back/internal/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OpenEugene/openboard/back/internal 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/codemodus/chain/v2 v2.1.2 7 | github.com/codemodus/hedrs v0.1.1 8 | github.com/codemodus/mixmux v0.2.1 9 | github.com/codemodus/sqlo v0.2.1 10 | github.com/codemodus/swagui v0.3.0 11 | github.com/codemodus/uidgen v0.1.0 12 | github.com/go-sql-driver/mysql v1.5.0 13 | github.com/golang/protobuf v1.4.3 14 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.2.0 15 | github.com/jmoiron/sqlx v1.3.1 16 | google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea 17 | google.golang.org/grpc v1.35.0 18 | google.golang.org/protobuf v1.25.0 19 | ) 20 | -------------------------------------------------------------------------------- /front/src/Session.elm: -------------------------------------------------------------------------------- 1 | module Session exposing (Session(..), navKey, viewer) 2 | 3 | import Browser.Navigation as Nav 4 | import Viewer exposing (Viewer) 5 | 6 | 7 | type Session 8 | = LoggedIn Nav.Key Viewer 9 | | Guest Nav.Key 10 | 11 | 12 | 13 | -- INFO 14 | 15 | 16 | viewer : Session -> Maybe Viewer 17 | viewer session = 18 | case session of 19 | LoggedIn _ val -> 20 | Just val 21 | 22 | Guest _ -> 23 | Nothing 24 | 25 | 26 | navKey : Session -> Nav.Key 27 | navKey session = 28 | case session of 29 | LoggedIn key _ -> 30 | key 31 | 32 | Guest key -> 33 | key 34 | -------------------------------------------------------------------------------- /back/cmd/openbsrv/README.md: -------------------------------------------------------------------------------- 1 | # openbsrv 2 | 3 | go get -u github.com/OpenEugene/openboard/back/cmd/openbsrv 4 | 5 | openbsrv is an application. This doc will be added to. 6 | 7 | ``` 8 | Usage of openbsrv: 9 | -dbaddr string 10 | database addr (default "127.0.0.1") 11 | -dbname string 12 | database name (default "openeug_openb_dev") 13 | -dbpass string 14 | database pass 15 | -dbport string 16 | database port (default ":3306") 17 | -dbuser string 18 | database user (default "openeug_openbdev") 19 | -frontdir string 20 | front public assets directory (default "../../../front/public") 21 | -migrate 22 | migrate up 23 | -rollback 24 | migrate dn 25 | -skipsrv 26 | skip server run 27 | -------------------------------------------------------------------------------- /back/cmd/openbsrv/httpsrv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/codemodus/hedrs" 5 | 6 | "github.com/OpenEugene/openboard/back/internal/httpsrv" 7 | ) 8 | 9 | type httpSrv struct { 10 | s *httpsrv.HTTPSrv 11 | 12 | rpcPort string 13 | httpPort string 14 | } 15 | 16 | func newHTTPSrv(rpcPort, httpPort string, origins []string) (*httpSrv, error) { 17 | hs, err := httpsrv.New(hedrs.DefaultOrigins) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | s := httpSrv{ 23 | s: hs, 24 | rpcPort: rpcPort, 25 | httpPort: httpPort, 26 | } 27 | 28 | return &s, nil 29 | } 30 | 31 | // Serve ... 32 | func (s *httpSrv) Serve() error { 33 | return s.s.Serve(s.rpcPort, s.httpPort) 34 | } 35 | 36 | // Stop ... 37 | func (s *httpSrv) Stop() error { 38 | return s.s.Stop() 39 | } 40 | -------------------------------------------------------------------------------- /front/public/boot.js: -------------------------------------------------------------------------------- 1 | var storageKey = 'store'; 2 | var flags = localStorage.getItem(storageKey); 3 | var app = Elm.Main.init({flags: flags}); 4 | // app.ports.storeCache.subscribe(function(val) { 5 | // if (val === null) { 6 | // localStorage.removeItem(storageKey); 7 | // } else { 8 | // localStorage.setItem(storageKey, JSON.stringify(val)); 9 | // } 10 | // // Report that the new session was stored successfully. 11 | // setTimeout(function() { 12 | // app.ports.onStoreChange.send(val); 13 | // }, 0); 14 | // }); 15 | 16 | // Whenever localStorage changes in another tab, report it if necessary. 17 | window.addEventListener('storage', function(event) { 18 | if (event.storageArea === localStorage && event.key === storageKey) { 19 | app.ports.onStoreChange.send(event.newValue); 20 | } 21 | }, false); -------------------------------------------------------------------------------- /back/internal/usersvc/internal/asset/assets/0001_setup_user_table.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | CREATE TABLE user ( 4 | user_id VARCHAR(26) NOT NULL PRIMARY KEY, 5 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 6 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 7 | deleted_at DATETIME DEFAULT NULL, 8 | 9 | username VARCHAR(255) NOT NULL UNIQUE, 10 | email VARCHAR(255) NOT NULL UNIQUE, 11 | email_hold BOOL NOT NULL DEFAULT TRUE, 12 | altmail VARCHAR(255) NOT NULL, 13 | altmail_hold BOOL NOT NULL DEFAULT TRUE, 14 | full_name VARCHAR(255) NOT NULL, 15 | avatar VARCHAR(255) NOT NULL DEFAULT "", 16 | password VARCHAR(255) NOT NULL, 17 | 18 | last_login DATETIME DEFAULT NULL, 19 | blocked_at DATETIME DEFAULT NULL 20 | ); 21 | 22 | -- +migrate Down 23 | 24 | DROP TABLE IF EXISTS user; 25 | -------------------------------------------------------------------------------- /back/tests/openbsrv/common_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "reflect" 5 | 6 | timestamp "github.com/golang/protobuf/ptypes/timestamp" 7 | ) 8 | 9 | func unsetUntestedFields(item interface{}) { 10 | val := reflect.Indirect(reflect.ValueOf(item)) 11 | if val.Kind() != reflect.Struct { 12 | return 13 | } 14 | 15 | strFldNames := []string{"Id"} 16 | for _, name := range strFldNames { 17 | fv := val.FieldByName(name) 18 | if fv.IsValid() && fv.Kind() == reflect.String && fv.CanSet() { 19 | fv.SetString("") 20 | } 21 | } 22 | 23 | timeFldNames := []string{ 24 | "LastLogin", 25 | "Created", 26 | "Updated", 27 | "Deleted", 28 | "Blocked", 29 | } 30 | t := new(timestamp.Timestamp) 31 | tt := reflect.TypeOf(t) 32 | 33 | for _, name := range timeFldNames { 34 | fv := val.FieldByName(name) 35 | if fv.IsValid() && fv.Type() == tt && fv.CanSet() { 36 | fv.Set(reflect.Zero(tt)) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /back/tools/iso/compose/poc/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | openbdb_mariadb: 5 | 6 | services: 7 | 8 | openbdb: 9 | environment: 10 | MYSQL_DATABASE: "${OPENBSRV_DBNAME}" 11 | MYSQL_USER: "${OPENBSRV_DBUSER}" 12 | MYSQL_PASSWORD: "${OPENBSRV_DBPASS}" 13 | MYSQL_ROOT_PASSWORD: "${OPENBSRV_DBPASS}" 14 | volumes: 15 | - "openbdb_mariadb:/var/lib/mysql" 16 | build: "../../file/openbdb" 17 | restart: "unless-stopped" 18 | 19 | openbsrv: 20 | depends_on: 21 | - "openbdb" 22 | environment: 23 | DBADDR: "openbdb" 24 | DBNAME: "${OPENBSRV_DBNAME}" 25 | DBUSER: "${OPENBSRV_DBUSER}" 26 | DBPASS: "${OPENBSRV_DBPASS}" 27 | MIGRATETYPE: "${MIGRATETYPE:-migrate}" 28 | ports: 29 | - "${OPENBSRV_APIPORT:-4243}:4243" 30 | - "${OPENBSRV_FRONTPORT:-4244}:4244" 31 | build: "../../file/openbsrv-full" 32 | restart: "unless-stopped" 33 | -------------------------------------------------------------------------------- /back/tests/openbsrv/run-tests: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | main() { 4 | local deferreds=() 5 | function runDeferreds() { 6 | for deferred in "${deferreds[@]}"; do eval ${deferred}; done 7 | } 8 | trap "runDeferreds" EXIT RETURN 9 | 10 | local rootDir="$(dirname "${0}")" 11 | pushd "${rootDir}" >/dev/null && rootDir="${PWD}" && popd >/dev/null || exit 1 12 | local command="$(basename "${rootDir}")" 13 | local projectDir="${rootDir}/../../cmd/${command}" 14 | local buildDir="${rootDir}/build" 15 | local buildFile="${buildDir}/${command}-test" 16 | 17 | pushd "${projectDir}" >/dev/null 18 | deferreds+=("popd >/dev/null") 19 | go build -o "${buildFile}" || exit 1 20 | deferreds+=("rm \"${buildFile}\"") 21 | 22 | "${buildFile}" ${@} -rollback -skipsrv || exit 1 23 | "${buildFile}" ${@} -migrate & 24 | deferreds+=("kill %1") 25 | 26 | pushd "${rootDir}" >/dev/null 27 | deferreds+=("popd >/dev/null") 28 | go test -v 29 | } 30 | 31 | main ${@} 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Openboard 2 | 3 | An open source version of Switchboard. 4 | 5 | ***Join us first and third Wednesday evenings for sprint wrap & plan, and each Tuesday evening for check-in. [EUG-Go](https://www.meetup.com/eug-go)*** 6 | 7 | *We are based in the Pacific Timezone, but all are welcome 8 | 9 | ## Overview 10 | 11 | - The [Front End](https://github.com/OpenEugene/openboard/tree/master/front) is 12 | written in Elm 13 | - Produces assets in the form of JavaScript artifacts 14 | - The [Back End](https://github.com/OpenEugene/openboard/tree/master/back) is 15 | written in Go 16 | - Produces an executable (*nix/Win) managing multiple servers 17 | - gRPC API server component 18 | - JSON/HTTP API server component 19 | - Front end asset server component 20 | 21 | ## Community 22 | 23 | - [Code of Conduct](./docs/CODE_OF_CONDUCT.md) 24 | - [Contributing](./docs/CONTRIBUTING.md) 25 | - [Authors](./docs/AUTHORS) 26 | - [Learning Walk-Throughs](./docs/learning/README.md) 27 | -------------------------------------------------------------------------------- /front/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.0", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.1", 10 | "elm/core": "1.0.2", 11 | "elm/html": "1.0.0", 12 | "elm/json": "1.1.3", 13 | "elm/time": "1.0.0", 14 | "elm/url": "1.0.0", 15 | "rtfeldman/elm-css": "16.0.0", 16 | "tiziano88/elm-protobuf": "3.0.0" 17 | }, 18 | "indirect": { 19 | "Skinney/murmur3": "2.0.8", 20 | "elm/regex": "1.0.0", 21 | "elm/virtual-dom": "1.0.2", 22 | "jweir/elm-iso8601": "5.0.2", 23 | "rtfeldman/elm-hex": "1.0.0" 24 | } 25 | }, 26 | "test-dependencies": { 27 | "direct": { 28 | "elm-explorations/test": "1.0.0" 29 | }, 30 | "indirect": { 31 | "elm/random": "1.0.0" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Welcome 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | -------------------------------------------------------------------------------- /front/src/Page.elm: -------------------------------------------------------------------------------- 1 | module Page exposing (Page(..), view) 2 | 3 | import Browser 4 | import Html.Styled exposing (..) 5 | import Viewer exposing (Viewer) 6 | import Route 7 | import Ui 8 | 9 | type Page 10 | = Home 11 | | Other 12 | 13 | 14 | 15 | -- Wraps the Pages 16 | 17 | 18 | view : Maybe Viewer -> Page -> { title : String, content : Html.Styled.Html msg } -> Browser.Document msg 19 | view maybeViewer page { title, content } = 20 | { title = title ++ " - Openboard" 21 | , body = Ui.globalStyle :: navBar page maybeViewer :: content :: [] |> List.map Html.Styled.toUnstyled 22 | } 23 | 24 | 25 | navBar page maybeViewer = 26 | Ui.navBar [] [ 27 | Ui.navBarList [] [ 28 | li [] [ 29 | Ui.linkBtn [ Route.href Route.Home ] [ text "OpenBoard" ] 30 | ] 31 | ] 32 | ] 33 | 34 | 35 | 36 | 37 | viewerAsString : Maybe Viewer -> String 38 | viewerAsString maybeV = 39 | case maybeV of 40 | Just v -> 41 | Viewer.unbox v 42 | 43 | Nothing -> 44 | "no viewer" 45 | -------------------------------------------------------------------------------- /back/cmd/openbsrv/srvmgmt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type server interface { 11 | Serve() error 12 | Stop() error 13 | } 14 | 15 | type serverMgmt struct { 16 | ss []server 17 | } 18 | 19 | func newServerMgmt(ss ...server) *serverMgmt { 20 | return &serverMgmt{ 21 | ss: ss, 22 | } 23 | } 24 | 25 | func (m *serverMgmt) serve() error { 26 | var wg sync.WaitGroup 27 | wg.Add(len(m.ss)) 28 | 29 | for _, s := range m.ss { 30 | go func(s server) { 31 | defer wg.Done() 32 | 33 | // TODO: gather returned errors 34 | if err := s.Serve(); err != nil { 35 | fmt.Fprintln(os.Stderr, "server error:", err) 36 | } 37 | }(s) 38 | 39 | time.Sleep(time.Millisecond * 200) 40 | } 41 | 42 | wg.Wait() 43 | 44 | return nil 45 | } 46 | 47 | func (m *serverMgmt) stop() error { 48 | for _, s := range m.ss { 49 | go func(s server) { 50 | // TODO: gather returned errors 51 | if err := s.Stop(); err != nil { 52 | fmt.Fprintln(os.Stderr, "stop error:", err) 53 | } 54 | }(s) 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /front/src/Page/Login.elm: -------------------------------------------------------------------------------- 1 | module Page.Login exposing (Model, Msg(..), init, toSession, update, view) 2 | 3 | import Html.Styled exposing (text) 4 | import Route 5 | import Session exposing (Session) 6 | import Ui 7 | 8 | 9 | type alias Model = 10 | { session : Session 11 | } 12 | 13 | 14 | type Msg 15 | = Noop 16 | 17 | 18 | init : Session -> ( Model, Cmd Msg ) 19 | init s = 20 | ( Model s, Cmd.none ) 21 | 22 | 23 | update : Msg -> Model -> ( Model, Cmd Msg ) 24 | update msg model = 25 | case msg of 26 | Noop -> 27 | ( model, Cmd.none ) 28 | 29 | 30 | view : Model -> { title : String, content : Html.Styled.Html Msg } 31 | view model = 32 | { title = "Login" 33 | , content = loginView model 34 | } 35 | 36 | 37 | loginView : Model -> Html.Styled.Html Msg 38 | loginView model = 39 | Ui.card [] 40 | [ Ui.heading "Login" 41 | , Ui.linkBtn [ Route.href Route.Home ] [ text "Home" ] 42 | , Ui.paragraph [] [ text <| Debug.toString model.session ] 43 | ] 44 | 45 | 46 | toSession : Model -> Session 47 | toSession { session } = 48 | session 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 The Openboard Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /back/cmd/openbsrv/frontsrv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/codemodus/alfred" 8 | "github.com/codemodus/chain/v2" 9 | "github.com/codemodus/hedrs" 10 | ) 11 | 12 | type frontSrv struct { 13 | s *http.Server 14 | } 15 | 16 | func newFrontSrv(port, dir string, origins []string) (*frontSrv, error) { 17 | origins = append(hedrs.DefaultOrigins, origins...) 18 | corsOrigins := hedrs.CORSOrigins(hedrs.NewAllowed(origins...)) 19 | corsMethods := hedrs.CORSMethods(hedrs.NewValues(hedrs.AllMethods...)) 20 | corsHeaders := hedrs.CORSHeaders(hedrs.NewValues(hedrs.DefaultHeaders...)) 21 | 22 | cmn := chain.New( 23 | corsOrigins, 24 | corsMethods, 25 | corsHeaders, 26 | ) 27 | 28 | s := frontSrv{ 29 | s: &http.Server{ 30 | Addr: port, 31 | Handler: cmn.End(alfred.New(dir)), 32 | }, 33 | } 34 | 35 | return &s, nil 36 | } 37 | 38 | func (s *frontSrv) Serve() error { 39 | if err := s.s.ListenAndServe(); err != nil && err != http.ErrServerClosed { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | func (s *frontSrv) Stop() error { 46 | // TODO: setup context 47 | return s.s.Shutdown(context.Background()) 48 | } 49 | -------------------------------------------------------------------------------- /back/tools/iso/file/openbsrv-full/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk add --update git libc6-compat 4 | 5 | RUN wget https://dl.google.com/go/go1.12.4.linux-amd64.tar.gz 6 | RUN tar -C /usr/local -xzf go*.tar.gz 7 | RUN rm go*.tar.gz 8 | ENV PATH=${PATH}:/usr/local/go/bin 9 | ENV GO111MODULE=on 10 | 11 | ARG codedir=/code 12 | ARG codebin=${codedir}/bin 13 | RUN mkdir -p ${codebin} 14 | ENV PATH=${PATH}:${codebin} 15 | 16 | ARG repo=OpenEugene/openboard 17 | ARG codesrc=${codedir}/src 18 | RUN mkdir -p ${codesrc}/${repo} 19 | RUN git clone https://github.com/${repo} ${codesrc}/${repo} 20 | WORKDIR ${codesrc}/${repo}/back/cmd/openbsrv 21 | RUN go build -o ${codebin}/openbsrv 22 | 23 | ENV DBADDR="" 24 | ENV DBPORT="" 25 | ENV DBNAME="" 26 | ENV DBUSER="" 27 | ENV DBPASS="" 28 | ENV MIGRATETYPE="" 29 | ENV FRONTDIR=${codesrc}/${repo}/front/public 30 | 31 | ENTRYPOINT openbsrv \ 32 | ${DBADDR/$DBADDR/-dbaddr=$DBADDR} \ 33 | ${DBPORT/$DBPORT/-dbport=$DBPORT} \ 34 | ${DBNAME/$DBNAME/-dbname=$DBNAME} \ 35 | ${DBUSER/$DBUSER/-dbuser=$DBUSER} \ 36 | ${DBPASS/$DBPASS/-dbpass=$DBPASS} \ 37 | ${MIGRATETYPE/$MIGRATETYPE/-$MIGRATETYPE} \ 38 | -frontdir=${FRONTDIR} 39 | 40 | EXPOSE 4243 4244 41 | -------------------------------------------------------------------------------- /msgs/proto/include/googleapis/google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/api/http.proto"; 20 | import "google/protobuf/descriptor.proto"; 21 | 22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "AnnotationsProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | extend google.protobuf.MethodOptions { 29 | // See `HttpRule`. 30 | HttpRule http = 72295728; 31 | } 32 | -------------------------------------------------------------------------------- /back/internal/log/log.go: -------------------------------------------------------------------------------- 1 | // Package log allows for outputting information regarding application errors 2 | // and events important to know for proper operation. 3 | package log 4 | 5 | import ( 6 | "io" 7 | "log" 8 | ) 9 | 10 | // Output is where logger will write to. Prefix is what will precede log text. 11 | type Output struct { 12 | Out io.Writer 13 | Prefix string 14 | } 15 | 16 | // Config allows for configuring the logger outputs for errors and information 17 | // type log messages. 18 | type Config struct { 19 | Err Output 20 | Inf Output 21 | } 22 | 23 | // Log is able to write information to a chosen output. 24 | type Log struct { 25 | err *log.Logger 26 | inf *log.Logger 27 | } 28 | 29 | // New provides a pointer to a new instance of the Log object. 30 | func New(c Config) *Log { 31 | return &Log{ 32 | inf: log.New(c.Inf.Out, c.Inf.Prefix, 0), 33 | err: log.New(c.Err.Out, c.Err.Prefix, 0), 34 | } 35 | } 36 | 37 | // Info outputs information from the application. 38 | func (log *Log) Info(format string, as ...interface{}) { 39 | log.inf.Printf(format+"\n", as...) 40 | } 41 | 42 | // Error outputs error information from the application. 43 | func (log *Log) Error(format string, as ...interface{}) { 44 | log.err.Printf(format+"\n", as...) 45 | } 46 | -------------------------------------------------------------------------------- /back/internal/authsvc/authsvc.go: -------------------------------------------------------------------------------- 1 | package authsvc 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc" 7 | 8 | "github.com/OpenEugene/openboard/back/internal/pb" 9 | ) 10 | 11 | var _ pb.AuthServer = &AuthSvc{} 12 | 13 | // AuthSvc ecapsulates dependencies and data required to implement the 14 | // pb.AuthServer interface. 15 | type AuthSvc struct { 16 | // TODO: implement AuthSvc 17 | } 18 | 19 | // New returns a pointer to an AuthSvc instance or an error. 20 | func New() (*AuthSvc, error) { 21 | return &AuthSvc{}, nil 22 | } 23 | 24 | // RegisterWithGRPCServer implements the grpcsrv.Registerable interface. 25 | func (s *AuthSvc) RegisterWithGRPCServer(g *grpc.Server) error { 26 | pb.RegisterAuthServer(g, s) 27 | 28 | return nil 29 | } 30 | 31 | // AddAuth implements part of the pb.AuthServer interface. 32 | func (s *AuthSvc) AddAuth(ctx context.Context, req *pb.AddAuthReq) (*pb.AuthResp, error) { 33 | // TODO: implement AddAuth 34 | return nil, nil 35 | } 36 | 37 | // RmvAuth implements part of the pb.AuthServer interface. 38 | func (s *AuthSvc) RmvAuth(ctx context.Context, req *pb.RmvAuthReq) (*pb.RmvAuthResp, error) { 39 | // TODO: implement RmvAuth 40 | 41 | return nil, nil 42 | } 43 | 44 | // AddVoucher implements part of the pb.AuthServer interface. 45 | func (s *AuthSvc) AddVoucher(ctx context.Context, req *pb.AddVoucherReq) (*pb.AddVoucherResp, error) { 46 | // TODO: implement AddVoucher 47 | 48 | return nil, nil 49 | } 50 | -------------------------------------------------------------------------------- /back/internal/dbg/dbg.go: -------------------------------------------------------------------------------- 1 | // Package dbg allows for outputting information that can help with debugging 2 | // the application. 3 | package dbg 4 | 5 | import ( 6 | "io" 7 | "log" 8 | "sync/atomic" 9 | ) 10 | 11 | // dbg is thread-safe. 12 | type dbg struct { 13 | atomVal atomic.Value 14 | } 15 | 16 | func new() *dbg { 17 | var d dbg 18 | var logr *log.Logger 19 | d.atomVal.Store(logr) 20 | return &d 21 | } 22 | 23 | func (d *dbg) logln(as ...interface{}) { 24 | logr := d.atomVal.Load().(*log.Logger) 25 | if logr != nil { 26 | logr.Println(as...) 27 | } 28 | } 29 | 30 | func (d *dbg) logf(format string, as ...interface{}) { 31 | logr := d.atomVal.Load().(*log.Logger) 32 | if logr != nil { 33 | logr.Printf(format+"\n", as...) 34 | } 35 | } 36 | 37 | func (d *dbg) setOut(out io.Writer) { 38 | var logr *log.Logger 39 | 40 | if out != nil { 41 | logr = log.New(out, "", 0) 42 | d.atomVal.Store(logr) 43 | return 44 | } 45 | 46 | d.atomVal.Store(logr) 47 | } 48 | 49 | var debug = new() 50 | 51 | // Log outputs information to help with application debugging. 52 | func Log(as ...interface{}) { 53 | debug.logln(as...) 54 | } 55 | 56 | // Logf outputs debugging information and is able to interpret formatting verbs. 57 | func Logf(format string, as ...interface{}) { 58 | debug.logf(format, as...) 59 | } 60 | 61 | // SetDebugOut allows for choosing where debug information will be written to. 62 | func SetOut(out io.Writer) { 63 | debug.setOut(out) 64 | } 65 | -------------------------------------------------------------------------------- /back/internal/grpcsrv/grpcsrv.go: -------------------------------------------------------------------------------- 1 | package grpcsrv 2 | 3 | import ( 4 | "fmt" 5 | 6 | "google.golang.org/grpc" 7 | ) 8 | 9 | // Registerable describes services able to be registered with a grpc.Server. 10 | type Registerable interface { 11 | RegisterWithGRPCServer(*grpc.Server) error 12 | } 13 | 14 | // GRPCSrv wraps a grpc.Server for convenience. 15 | type GRPCSrv struct { 16 | *grpc.Server 17 | } 18 | 19 | // New returns a pointer to a basic GRPCSrv instance or an error. 20 | func New() (*GRPCSrv, error) { 21 | opts := []grpc.ServerOption{} 22 | 23 | s := GRPCSrv{ 24 | Server: grpc.NewServer(opts...), 25 | } 26 | 27 | return &s, nil 28 | } 29 | 30 | // RegisterServices registers services with the underlying grpc.Server. 31 | func (s *GRPCSrv) RegisterServices(rs ...Registerable) error { 32 | for _, r := range rs { 33 | if err := r.RegisterWithGRPCServer(s.Server); err != nil { 34 | return fmt.Errorf("cannot register service: %s", err) 35 | } 36 | } 37 | 38 | return nil 39 | } 40 | 41 | // Serve sets up a tcp listener on the provided port and serves the underlying 42 | // grpc.Server instance. 43 | func (s *GRPCSrv) Serve(port string) error { 44 | we := func(err error) error { 45 | return fmt.Errorf("cannot serve: %s", err) 46 | } 47 | 48 | l, err := tcpListener(port) 49 | if err != nil { 50 | return we(err) 51 | } 52 | 53 | if err = s.Server.Serve(l); err != nil { 54 | // TODO: if err "contains closed" err, return nil 55 | 56 | return we(err) 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /msgs/proto/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // openboard 4 | // 5 | // auth semantics: 6 | // 7 | // Add{T}(s) (Add{T}(s)Req) returns {T}(s)Resp // POST 8 | // Ovr{T}(s) (Ovr{T}(s)Req) returns {T}(s)Resp // PUT 9 | // Mod{T}(s) (Mod{T}(s)Req) returns {T}(s)Resp // PATCH 10 | // Get{T} (Get{T}Req) returns {T}Resp // GET 11 | // Fnd{T}s (Fnd{T}sReq) returns {T}sResp // GET 12 | // Rmv{T}(s) (Rmv{T}(s)Req) returns EmptyResp // DELETE 13 | // Unr{T} (Unr{T}Req) returns {T}Resp // PATCH 14 | package pb; 15 | 16 | option go_package = ".;pb"; 17 | 18 | import "google/api/annotations.proto"; 19 | 20 | message AuthResp { string token = 1; } 21 | 22 | message AddAuthReq { 23 | string username = 1; 24 | string password = 2; 25 | } 26 | 27 | message RmvAuthResp {} 28 | 29 | message RmvAuthReq { string token = 1; } 30 | 31 | message AddVoucherResp {} 32 | 33 | message AddVoucherReq { 34 | bool notify = 1; 35 | string email = 2; 36 | bool password = 3; 37 | } 38 | 39 | service Auth { 40 | rpc AddAuth(AddAuthReq) returns (AuthResp) { 41 | option (google.api.http) = { 42 | post : "/auth" 43 | body : "*" 44 | }; 45 | } 46 | 47 | rpc RmvAuth(RmvAuthReq) returns (RmvAuthResp) { 48 | option (google.api.http) = { 49 | delete : "/auth/{token}" 50 | }; 51 | } 52 | 53 | rpc AddVoucher(AddVoucherReq) returns (AddVoucherResp) { 54 | option (google.api.http) = { 55 | post : "/voucher" 56 | body : "*" 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /back/internal/dbg/dbg_test.go: -------------------------------------------------------------------------------- 1 | package dbg 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "testing" 8 | ) 9 | 10 | func BenchmarkDbgUse(b *testing.B) { 11 | SetOut(ioutil.Discard) 12 | b.RunParallel(func(pb *testing.PB) { 13 | for pb.Next() { 14 | Log("") 15 | } 16 | }) 17 | } 18 | func BenchmarkDbgUseNil(b *testing.B) { 19 | b.RunParallel(func(pb *testing.PB) { 20 | for pb.Next() { 21 | Log("") 22 | } 23 | }) 24 | } 25 | func BenchmarkDbgSetAndUse(b *testing.B) { 26 | b.RunParallel(func(pb *testing.PB) { 27 | for pb.Next() { 28 | SetOut(ioutil.Discard) 29 | Log("") 30 | } 31 | }) 32 | } 33 | func BenchmarkDbgSetAndUseNil(b *testing.B) { 34 | b.RunParallel(func(pb *testing.PB) { 35 | for pb.Next() { 36 | SetOut(nil) 37 | Log("") 38 | } 39 | }) 40 | } 41 | 42 | func TestSetOut(t *testing.T) { 43 | Log("debug not set") 44 | 45 | buff := bytes.NewBuffer([]byte{}) 46 | SetOut(buff) 47 | msg := "debug set to bytes buffer" 48 | Log(msg) 49 | got := buff.String() 50 | 51 | want := msg + "\n" 52 | if got != want { 53 | t.Errorf("want: %s, got: %s", want, got) 54 | } 55 | 56 | SetOut(nil) 57 | buff.Reset() 58 | want = "" 59 | Log("debug set to nil") 60 | got = buff.String() 61 | if got != want { 62 | t.Errorf("want: nothing, got: %s", got) 63 | } 64 | 65 | SetOut(buff) 66 | buff.Reset() 67 | msg = "debug set to bytes buffer, %s time" 68 | Logf(msg, "second") 69 | got = buff.String() 70 | want = fmt.Sprintf(msg+"\n", "second") 71 | if got != want { 72 | t.Errorf("want: %s, got: %s", want, got) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /front/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Welcome! Glad you are here. 4 | 5 | ## Installation 6 | 7 | First step would be to [install Elm](https://guide.elm-lang.org/install.html). 8 | 9 | Then you should have the `elm` command available. To compile the application run: 10 | 11 | `elm make src/Main.elm --output=public/app.js --debug` 12 | 13 | *Please be sure to add `**/elm-stuff/` to your global git ignore file. 14 | 15 | ## Get started 16 | 17 | You can serve up the frontend on its own, or use spin the whole stack up together (see top level README) 18 | 19 | ``` 20 | npm i -g elm-live 21 | cd front/ 22 | elm-live src/Main.elm -u --open --dir=public -- --output=public/app.js --debug 23 | ``` 24 | 25 | ## Tests 26 | 27 | Check out `tests/Tests.elm`. 28 | 29 | ``` 30 | npm i -g elm-test 31 | elm-test 32 | ``` 33 | 34 | ## Protobuf 35 | 36 | Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data. Learn More(https://developers.google.com/protocol-buffers/). 37 | 38 | To generate type aliases, encoders and decoders run `./genelm`. This will read all the `.proto` files and generate corresponding `.elm` files. This command should be run frequently and updates to the `Proto/` modules should have there own commit. This gives us a type safe border between the client and the server. If something changes on the server — a field is added or removed, a data model is added or removed — our Elm app should fail to compile. No more `400 Bad Request` at runtime! 39 | 40 | ## Contributing 41 | 42 | - [Start here](../docs/CONTRIBUTING.md) 43 | -------------------------------------------------------------------------------- /back/tools/install-tools: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | install-protoc() { 4 | local binDest="${1}" 5 | local includeDest="${2}" 6 | local version="${3}" 7 | local ostype="$([[ "${OSTYPE}" == darwin* ]] && echo osx || echo linux)" 8 | local arch="$(uname -m)" 9 | local pfmt="https://github.com/protocolbuffers/protobuf/releases/download/v%s/protoc-%s-%s-%s.zip" 10 | local path="$(printf "${pfmt}" "${version}" "${version}" "${ostype}" "${arch}")" 11 | local tmpDir="$(mktemp -d)" 12 | local zipFile="${tmpDir}/protoc.zip" 13 | 14 | wget -O "${zipFile}" "${path}" 15 | 16 | unzip -j ${zipFile} bin/protoc -d "${binDest}" 17 | 18 | unzip ${zipFile} include/'*' -d "${includeDest}" 19 | mv "${includeDest}/include/"* "${includeDest}" 20 | rm -rf "${includeDest}/include" 21 | } 22 | 23 | main() { 24 | local rootDir="$(cd "$(dirname "${0}")" && echo "${PWD}")" 25 | local binDir="${rootDir}/.bin" 26 | local includeDir="${rootDir}/.include" 27 | 28 | mkdir -p "${binDir}" 29 | pushd "${binDir}" &> /dev/null && rm "${binDir}/"* 30 | 31 | export GOBIN="${PWD}" 32 | go install github.com/codemodus/withdraw@v0.1.0 33 | go install github.com/go-bindata/go-bindata/go-bindata@v3.1.1 34 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.25.0 35 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1.0 36 | go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.2.0 37 | go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.2.0 38 | 39 | rm -rf "${includeDir}/"* 40 | install-protoc "${PWD}" "${includeDir}" 3.17.0 41 | 42 | popd &> /dev/null 43 | } 44 | 45 | main 46 | -------------------------------------------------------------------------------- /front/src/Page/Home.elm: -------------------------------------------------------------------------------- 1 | module Page.Home exposing (Model, Msg(..), init, toSession, update, view) 2 | 3 | import Html.Styled exposing (..) 4 | import Html.Styled.Events exposing (onClick) 5 | import Route 6 | import Session exposing (Session) 7 | import Ui 8 | 9 | 10 | type alias Model = 11 | { session : Session 12 | , greeting : String 13 | } 14 | 15 | 16 | type Msg 17 | = InternalHomeMsg 18 | 19 | 20 | init : Session -> ( Model, Cmd Msg ) 21 | init s = 22 | ( Model s "Change me", Cmd.none ) 23 | 24 | 25 | update : Msg -> Model -> ( Model, Cmd Msg ) 26 | update msg model = 27 | case msg of 28 | InternalHomeMsg -> 29 | ( { model | greeting = "Hello from Page.Home" }, Cmd.none ) 30 | 31 | 32 | view : Model -> { title : String, content : Html.Styled.Html Msg } 33 | view model = 34 | { title = "Home" 35 | , content = homeView model 36 | } 37 | 38 | 39 | homeView : Model -> Html.Styled.Html Msg 40 | homeView model = 41 | Ui.mainContent [] 42 | [ Ui.flexBox [] 43 | [ Ui.linkBtn [ Route.href Route.NewRequest ] [ text "Request" ] 44 | , Ui.linkBtn [ Route.href Route.NewOffer ] [ text "Offer" ] 45 | ] 46 | , Ui.postingsList [] 47 | (List.map Ui.postingBlurb 48 | [ { slug = "1", title = "Golang Needed", body = "A bunch of text blablablaosdihf osidhf sdoifh sdfs df...." } 49 | , { slug = "2", title = "Another Posting Example", body = "lsjkdfno sidhsdf sdf sdfhj osd fsdf sdf sdf sdfmore some posting stuff" } 50 | ] 51 | ) 52 | ] 53 | 54 | 55 | toSession : Model -> Session 56 | toSession { session } = 57 | session 58 | -------------------------------------------------------------------------------- /back/tools/install-go: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | gover="$(\ 5 | curl -s https://golang.org/dl/ | \ 6 | grep -o 'go1\.[0-9]\{1,2\}\.[0-9]' | \ 7 | sort -t. -k2 -h | \ 8 | tail -1 \ 9 | )" 10 | rmvold="" 11 | 12 | if hash go 2>/dev/null; then 13 | gver=${gover#go1.} 14 | goins="$(go version | cut -d " " -f 3)" 15 | gins="${goins#go1.}" 16 | [[ "${goins}" == "" ]] && echo >&2 "cannot find installed go version" && exit 1 17 | 18 | msg="go is already installed and updated" 19 | [[ "$(echo "${gins} >= ${gver}" | bc -l)" == "1" ]] && echo "${msg}" && exit 0 20 | 21 | [[ -d "/usr/local/go" ]] && sudo mv "/usr/local/go" "/usr/local/${goins}" 22 | echo "remove previous go installation (${goins})? [y/N]" 23 | read rmvold 24 | fi 25 | 26 | ostype="$([[ "${OSTYPE}" == "darwin"* ]] && echo "darwin" || echo "linux")" 27 | 28 | if ! hash git 2>/dev/null; then 29 | [[ "${ostype}" == "linux" ]] && sudo apt update && sudo apt install git 30 | [[ "${ostype}" == "darwin" ]] && brew update && brew install git 31 | fi 32 | 33 | gorel="${gover}.${ostype}-amd64.tar.gz" 34 | 35 | pushd /usr/local >/dev/null 36 | sudo curl -O https://dl.google.com/go/${gorel} 37 | sudo tar -C /usr/local -zxf ${gorel} 38 | sudo rm ${gorel} 39 | popd >/dev/null 40 | 41 | srcpro="false" 42 | if ! grep -iP "PATH=.*?go.*?bin" ${HOME}/.profile >/dev/null; then 43 | echo 'PATH=${PATH}:/usr/local/go/bin' >> ${HOME}/.profile 44 | srcpro="true" 45 | fi 46 | if ! grep "GO111MODULE=" ${HOME}/.profile >/dev/null; then 47 | echo 'export GO111MODULE=on' >> ${HOME}/.profile 48 | srcpro="true" 49 | fi 50 | 51 | [[ "${rmvold}" =~ ^y|Y$ ]] && sudo rm -rf "/usr/local/${goins}" 52 | [[ "${srcpro}" == "true" ]] && echo "please run 'source ~/.profile'" 53 | -------------------------------------------------------------------------------- /back/cmd/openbsrv/grpcsrv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/OpenEugene/openboard/back/internal/authsvc" 7 | "github.com/OpenEugene/openboard/back/internal/grpcsrv" 8 | "github.com/OpenEugene/openboard/back/internal/postsvc" 9 | "github.com/OpenEugene/openboard/back/internal/usersvc" 10 | ) 11 | 12 | type grpcSrv struct { 13 | s *grpcsrv.GRPCSrv 14 | 15 | port string 16 | svcs []interface{} 17 | } 18 | 19 | func newGRPCSrv(port string, db *sql.DB, drvr string) (*grpcSrv, error) { 20 | auth, err := authsvc.New() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | user, err := usersvc.New(db, drvr, 123456) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | post, err := postsvc.New(db, drvr, 123456) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | svcs := []interface{}{ 36 | auth, user, post, 37 | } 38 | 39 | gs, err := grpcsrv.New() 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | if err := registerServices(gs, svcs...); err != nil { 45 | return nil, err 46 | } 47 | 48 | s := grpcSrv{ 49 | s: gs, 50 | port: port, 51 | svcs: svcs, 52 | } 53 | 54 | return &s, nil 55 | } 56 | 57 | func (s *grpcSrv) services() []interface{} { 58 | return s.svcs 59 | } 60 | 61 | func (s *grpcSrv) Serve() error { 62 | return s.s.Serve(s.port) 63 | } 64 | 65 | func (s *grpcSrv) Stop() error { 66 | s.s.GracefulStop() 67 | return nil 68 | } 69 | 70 | func registerServices(srv *grpcsrv.GRPCSrv, svcs ...interface{}) error { 71 | for _, svc := range svcs { 72 | if s, ok := svc.(grpcsrv.Registerable); ok { 73 | if err := srv.RegisterServices(s); err != nil { 74 | return err 75 | } 76 | } 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /front/src/Route.elm: -------------------------------------------------------------------------------- 1 | module Route exposing (Route(..), fromUrl, href) 2 | 3 | import Html.Styled exposing (Attribute) 4 | import Html.Styled.Attributes as Attr 5 | import Url exposing (Url) 6 | import Url.Parser as Parser exposing ((), Parser, oneOf, s, string) 7 | 8 | 9 | 10 | -- ROUTING 11 | 12 | 13 | type Route 14 | = Home 15 | | Login 16 | | NewOffer 17 | | NewRequest 18 | | PostDetail String 19 | | EditPost String 20 | 21 | 22 | parser : Parser (Route -> a) a 23 | parser = 24 | oneOf 25 | [ Parser.map Home Parser.top 26 | , Parser.map Login (s "login") 27 | , Parser.map NewOffer (s "offer" s "new") 28 | , Parser.map NewRequest (s "request" s "new") 29 | , Parser.map PostDetail (s "posts" string) 30 | , Parser.map EditPost (s "posts" string s "edit") 31 | ] 32 | 33 | 34 | 35 | -- PUBLIC HELPERS 36 | 37 | 38 | href : Route -> Attribute msg 39 | href targetRoute = 40 | Attr.href (routeToString targetRoute) 41 | 42 | 43 | fromUrl : Url -> Maybe Route 44 | fromUrl url = 45 | -- The RealWorld spec treats the fragment like a path. 46 | -- This makes it *literally* the path, so we can proceed 47 | -- with parsing as if it had been a normal path all along. 48 | { url | path = Maybe.withDefault "" url.fragment, fragment = Nothing } 49 | |> Parser.parse parser 50 | 51 | 52 | 53 | -- INTERNAL 54 | 55 | 56 | routeToString : Route -> String 57 | routeToString page = 58 | let 59 | pieces = 60 | case page of 61 | Home -> 62 | [] 63 | 64 | Login -> 65 | [ "login" ] 66 | 67 | NewRequest -> 68 | [ "request", "new" ] 69 | 70 | NewOffer -> 71 | [ "offer", "new" ] 72 | 73 | PostDetail string -> 74 | [ "posts", string ] 75 | 76 | EditPost string -> 77 | [ "posts", string, "edit" ] 78 | in 79 | "#/" ++ String.join "/" pieces 80 | -------------------------------------------------------------------------------- /back/cmd/openbsrv/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/codemodus/sqlmig" 10 | _ "github.com/go-sql-driver/mysql" 11 | ) 12 | 13 | func newSQLDB(driver, creds string) (*sql.DB, error) { 14 | db, err := sql.Open(driver, creds) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | db.SetMaxIdleConns(128) 20 | db.SetConnMaxLifetime(time.Hour) 21 | 22 | return db, patientPing(db) 23 | } 24 | 25 | func patientPing(db *sql.DB) error { 26 | limit := time.Second * 3 27 | pause := time.Millisecond * 500 28 | iters := 5 // 0 + .5 + 1 + 2 + 4 29 | var err error 30 | 31 | for i := 0; i < iters; i++ { 32 | if i > 0 { 33 | time.Sleep(pause) 34 | pause = pause * 2 35 | } 36 | 37 | func() { 38 | ctx, cancel := context.WithTimeout(context.Background(), limit) 39 | defer cancel() 40 | 41 | err = db.PingContext(ctx) 42 | }() 43 | 44 | if err == nil { 45 | return nil 46 | } 47 | } 48 | 49 | return err 50 | } 51 | 52 | type dbmig struct { 53 | m *sqlmig.SQLMig 54 | } 55 | 56 | func newDBMig(db *sql.DB, driver, tablePrefix string) (*dbmig, error) { 57 | mig, err := sqlmig.New(db, driver, tablePrefix) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | dbm := dbmig{ 63 | m: mig, 64 | } 65 | 66 | return &dbm, nil 67 | } 68 | 69 | func (m *dbmig) addMigrators(us ...interface{}) { 70 | for _, u := range us { 71 | if p, ok := u.(sqlmig.DataProvider); ok { 72 | m.m.AddDataProviders(p) 73 | } 74 | 75 | if r, ok := u.(sqlmig.Regularizer); ok { 76 | m.m.AddRegularizers(r) 77 | } 78 | } 79 | } 80 | 81 | func (m *dbmig) run(migrate, rollback bool) (sqlmig.Results, string) { 82 | switch { 83 | case migrate && rollback: 84 | return sqlmig.Results{}, "" 85 | case migrate: 86 | return m.m.Migrate(), "migrated" 87 | case rollback: 88 | return m.m.RollBack(), "rolled back" 89 | default: 90 | return sqlmig.Results{}, "" 91 | } 92 | } 93 | 94 | func dbCreds(name, user, pass, addr, port string) string { 95 | return fmt.Sprintf("%s:%s@tcp(%s%s)/%s?parseTime=true", user, pass, addr, port, name) 96 | } 97 | -------------------------------------------------------------------------------- /back/tools/install-mariadb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | OSLIN="linux" 5 | OSDAR="darwin" 6 | ostype="$([[ "${OSTYPE}" == ${OSDAR}* ]] && echo ${OSDAR} || echo ${OSLIN})" 7 | OSWSL="linux-wsl" 8 | [[ "${ostype}" == ${OSLIN} ]] && grep -qi "Microsoft\|WSL" /proc/version && ostype=${OSWSL} 9 | 10 | 11 | if ! hash mysql 2>/dev/null; then 12 | case ${ostype} in 13 | ${OSLIN}*) 14 | sudo apt update 15 | sudo apt install software-properties-common 16 | 17 | pkey="0xF1656F24C74CD1D8" 18 | case ${ostype} in 19 | ${OSLIN}) 20 | keysrv="hkp://keyserver.ubuntu.com:80" 21 | sudo apt-key adv --recv-keys --keyserver "${keysrv}" "${pkey}" 22 | ;; 23 | ${OSWSL}) 24 | keysrch="http://keyserver.ubuntu.com/pks/lookup?op=get&search=${pkey}" 25 | curl -sL "${keysrch}" | sudo apt-key add 26 | ;; 27 | esac 28 | repourl=http://sfo1.mirrors.digitalocean.com/mariadb/repo/10.3/ubuntu 29 | sudo add-apt-repository "deb [arch=amd64] ${repourl} $(lsb_release -cs) main" 30 | sudo apt update 31 | sudo apt install mariadb-server 32 | ;; 33 | ${OSDAR}) 34 | brew update 35 | brew install mariadb 36 | ;; 37 | esac 38 | 39 | sudo tee "/etc/mysql/mariadb.conf.d/60-encoding.cnf" >/dev/null << EOF 40 | [mysqld] 41 | character-set-server = utf8mb4 42 | collation-server = utf8mb4_unicode_ci 43 | character_set_server = utf8mb4 44 | collation_server = utf8mb4_unicode_ci 45 | EOF 46 | 47 | case ${ostype} in 48 | ${OSLIN}*) 49 | sudo service mysql restart 50 | ;; 51 | ${OSDAR}) 52 | mysql.server start 53 | brew services start mariadb 54 | ;; 55 | esac 56 | fi 57 | 58 | adddb="" 59 | rootpass="" 60 | echo "add database? [y/N]" 61 | read adddb 62 | [[ ! ${adddb} =~ y|Y ]] && exit 0 63 | echo "mysql root pass:" 64 | read -s rootpass 65 | 66 | dbname="" 67 | dbuser="" 68 | dbpass="" 69 | echo "database name:" 70 | read dbname 71 | echo "database user:" 72 | read dbuser 73 | echo "database pass:" 74 | read -s dbpass 75 | 76 | mysql -uroot -p"${rootpass}" << EOF 77 | CREATE DATABASE ${dbname} DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; 78 | GRANT ALL PRIVILEGES ON ${dbname}.* TO ${dbuser}@'%' IDENTIFIED BY '${dbpass}'; 79 | EOF 80 | -------------------------------------------------------------------------------- /front/src/Page/Posts.elm: -------------------------------------------------------------------------------- 1 | module Page.Posts exposing (Kind(..), Model, Msg(..), init, postsView, toSession, update, view) 2 | 3 | import Html.Styled exposing (button, form, input, label, text, textarea) 4 | import Html.Styled.Attributes exposing (type_) 5 | import Html.Styled.Events exposing (onInput) 6 | import Route 7 | import Session exposing (Session) 8 | import Ui 9 | 10 | 11 | type alias Model = 12 | { session : Session 13 | , title : String 14 | , body : String 15 | , kind : Kind 16 | , slug : String 17 | } 18 | 19 | 20 | type Kind 21 | = Offer 22 | | Request 23 | | Unknown 24 | 25 | 26 | type Msg 27 | = SetTitle String 28 | | SetBody String 29 | 30 | 31 | init : Session -> Kind -> String -> ( Model, Cmd Msg ) 32 | init session kind slug = 33 | ( Model session "" "" kind slug, Cmd.none ) 34 | 35 | 36 | update : Msg -> Model -> ( Model, Cmd Msg ) 37 | update msg model = 38 | case msg of 39 | SetTitle t -> 40 | ( { model | title = t }, Cmd.none ) 41 | 42 | SetBody b -> 43 | ( { model | body = b }, Cmd.none ) 44 | 45 | 46 | view : Model -> { title : String, content : Html.Styled.Html Msg } 47 | view model = 48 | { title = "Posts" 49 | , content = postsView model 50 | } 51 | 52 | 53 | heading : Kind -> Html.Styled.Html Msg 54 | heading kind = 55 | case kind of 56 | Offer -> 57 | Ui.heading "New Offer" 58 | 59 | Request -> 60 | Ui.heading "New Request" 61 | 62 | Unknown -> 63 | Ui.heading "Impossible!!" 64 | 65 | 66 | postsView : Model -> Html.Styled.Html Msg 67 | postsView model = 68 | Ui.card [] 69 | [ heading model.kind 70 | , Ui.linkBtn [ Route.href Route.Home ] [ text "Home" ] 71 | , form [] 72 | [ label [] 73 | [ text "Title" 74 | , input [ type_ "text", onInput SetTitle ] [] 75 | ] 76 | , label [] 77 | [ text "Body" 78 | , textarea [ onInput SetBody ] [] 79 | ] 80 | , button [] [ text "submit" ] 81 | ] 82 | ] 83 | 84 | 85 | toSession : Model -> Session 86 | toSession { session } = 87 | session 88 | -------------------------------------------------------------------------------- /back/tools/iso/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function ensureDocker() { 4 | OSLIN="linux" 5 | OSDAR="darwin" 6 | ostype="$([[ "${OSTYPE}" == ${OSDAR}* ]] && echo ${OSDAR} || echo ${OSLIN})" 7 | 8 | if ! hash docker 2>/dev/null; then 9 | case ${ostype} in 10 | ${OSLIN}) 11 | sudo apt install docker.io 12 | ;; 13 | ${OSDAR}) 14 | brew install docker || echo 15 | ;; 16 | esac 17 | [[ "$?" != "0" ]] && echo >&2 "cannot install docker" && exit 1 18 | fi 19 | 20 | if ! hash docker-compose 2>/dev/null; then 21 | case ${ostype} in 22 | ${OSLIN}) 23 | sudo easy_install pip 24 | pip install --user docker-compose 25 | ;; 26 | ${OSDAR}) 27 | brew install docker-compose 28 | ;; 29 | esac 30 | [[ "$?" != "0" ]] && echo >&2 "cannot install docker-compose" && exit 1 31 | fi 32 | } 33 | 34 | function main() { 35 | local dir="${1:-up}" 36 | local proj="dev" 37 | local svc="openbdb" 38 | local file="./compose/poc/docker-compose.yml" 39 | 40 | export OPENBSRV_DBNAME="${OPENBSRV_DBNAME:-openeug_openb_dev}" 41 | export OPENBSRV_DBUSER="${OPENBSRV_DBUSER:-openeug_openbdev}" 42 | export OPENBSRV_DBPASS="${OPENBSRV_DBPASS:-dummy}" 43 | 44 | case "${dir}" in 45 | up) 46 | if [[ "${OPENBSRV_DBPASS}" == "dummy" ]]; then 47 | local dbpass 48 | echo "database pass:" 49 | read -s dbpass 50 | export OPENBSRV_DBPASS="${dbpass}" 51 | fi 52 | 53 | docker-compose \ 54 | --file "${file}" \ 55 | --project-name "${proj}" \ 56 | up \ 57 | --detach \ 58 | "${svc}" 59 | [[ "$?" != "0" ]] && echo >&2 "cannot setup containers" && exit 1 60 | 61 | main ip 62 | ;; 63 | down|dn|clean) 64 | docker-compose \ 65 | --file "${file}" \ 66 | --project-name "${proj}" \ 67 | down 68 | 69 | [[ "${dir}" != "clean" ]] && exit 0 70 | 71 | docker image rm "${proj}_${svc}" 72 | 73 | docker volume rm \ 74 | "$(docker volume ls --filter "name=${proj}_${svc}" -q)" 75 | ;; 76 | ip) 77 | 78 | docker inspect \ 79 | --format \ 80 | '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ 81 | "$(docker ps --filter "name=${proj}_${svc}" -q | head -1)" 82 | ;; 83 | *) 84 | echo >&2 "must be a valid subcmd [up|dn|ip|clean]" 85 | exit 1 86 | ;; 87 | esac 88 | } 89 | 90 | ensureDocker 91 | main "${1}" 92 | -------------------------------------------------------------------------------- /back/internal/postsvc/internal/postdb/postdb.go: -------------------------------------------------------------------------------- 1 | package postdb 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/OpenEugene/openboard/back/internal/pb" 8 | "github.com/codemodus/sqlo" 9 | "github.com/codemodus/uidgen" 10 | ) 11 | 12 | var _ pb.PostServer = &PostDB{} 13 | 14 | // PostDB encapsulates dependencies and data required to implement the 15 | // pb.PostServer interface. 16 | type PostDB struct { 17 | db *sqlo.SQLO 18 | drv string 19 | ug *uidgen.UIDGen 20 | } 21 | 22 | // New returns a pointer to a PostDB instance or an error. 23 | func New(relDB *sql.DB, driver string, offset uint64) (*PostDB, error) { 24 | db := PostDB{ 25 | db: sqlo.New(relDB), 26 | drv: driver, 27 | ug: uidgen.New(offset, uidgen.VARCHAR26), 28 | } 29 | 30 | return &db, nil 31 | } 32 | 33 | // AddType implements part of the pb.PostServer interface. 34 | func (s *PostDB) AddType(ctx context.Context, req *pb.AddTypeReq) (*pb.TypeResp, error) { 35 | r := &pb.TypeResp{} 36 | if err := s.upsertType(ctx, "", req, r); err != nil { 37 | return nil, err 38 | } 39 | return r, nil 40 | } 41 | 42 | // FndTypes implements part of the pb.PostServer interface. 43 | func (s *PostDB) FndTypes(ctx context.Context, req *pb.FndTypesReq) (*pb.TypesResp, error) { 44 | r := &pb.TypesResp{} 45 | if err := s.findTypes(ctx, req, r); err != nil { 46 | return nil, err 47 | } 48 | return r, nil 49 | } 50 | 51 | // AddPost implements part of the pb.PostServer interface. 52 | func (s *PostDB) AddPost(ctx context.Context, req *pb.AddPostReq) (*pb.PostResp, error) { 53 | r := &pb.PostResp{} 54 | if err := s.upsertPost(ctx, "", req, r); err != nil { 55 | return nil, err 56 | } 57 | return r, nil 58 | } 59 | 60 | // FndPosts implements part of the pb.PostServer interface. 61 | func (s *PostDB) FndPosts(ctx context.Context, req *pb.FndPostsReq) (*pb.PostsResp, error) { 62 | r := &pb.PostsResp{} 63 | if err := s.findPosts(ctx, req, r); err != nil { 64 | return nil, err 65 | } 66 | return r, nil 67 | } 68 | 69 | // OvrPost implements part of the pb.PostServer interface. 70 | func (s *PostDB) OvrPost(ctx context.Context, req *pb.OvrPostReq) (*pb.PostResp, error) { 71 | r := &pb.PostResp{} 72 | if err := s.upsertPost(ctx, string(req.Id), req.Req, r); err != nil { 73 | return nil, err 74 | } 75 | return r, nil 76 | } 77 | 78 | // RmvPost implements part of the pb.PostServer interface. 79 | func (s *PostDB) RmvPost(ctx context.Context, req *pb.RmvPostReq) (*pb.RmvPostResp, error) { 80 | if err := s.deletePost(ctx, string(req.Id)); err != nil { 81 | return nil, err 82 | } 83 | return &pb.RmvPostResp{}, nil 84 | } 85 | -------------------------------------------------------------------------------- /back/internal/usersvc/internal/userdb/userdb.go: -------------------------------------------------------------------------------- 1 | package userdb 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/OpenEugene/openboard/back/internal/pb" 8 | "github.com/codemodus/uidgen" 9 | "github.com/jmoiron/sqlx" 10 | ) 11 | 12 | var _ pb.UserSvcServer = &UserDB{} 13 | 14 | // UserDB encapsulates dependencies and data required to implement the 15 | // pb.UserServer interface. 16 | type UserDB struct { 17 | db *sqlx.DB 18 | drv string 19 | ug *uidgen.UIDGen 20 | } 21 | 22 | // New returns a pointer to a UserDB instance or an error. 23 | func New(relDB *sql.DB, driver string, offset uint64) (*UserDB, error) { 24 | db := UserDB{ 25 | db: sqlx.NewDb(relDB, driver), 26 | drv: driver, 27 | ug: uidgen.New(offset, uidgen.VARCHAR26), 28 | } 29 | 30 | return &db, nil 31 | } 32 | 33 | // AddUser implements part of the pb.UserServer interface. 34 | func (s *UserDB) AddUser(ctx context.Context, req *pb.AddUserReq) (*pb.UserResp, error) { 35 | r := &pb.UserResp{} 36 | if err := s.upsertUser(ctx, "", req, r); err != nil { 37 | return nil, err 38 | } 39 | return r, nil 40 | } 41 | 42 | // OvrUser implements part of the pb.UserServer interface. 43 | func (s *UserDB) OvrUser(ctx context.Context, req *pb.OvrUserReq) (*pb.UserResp, error) { 44 | r := &pb.UserResp{} 45 | if err := s.upsertUser(ctx, string(req.Id), req.Req, r); err != nil { 46 | return nil, err 47 | } 48 | return r, nil 49 | } 50 | 51 | // RmvUser implements part of the pb.UserServer interface. 52 | func (s *UserDB) RmvUser(ctx context.Context, req *pb.RmvUserReq) (*pb.RmvUserResp, error) { 53 | if err := s.deleteUser(ctx, string(req.Id)); err != nil { 54 | return nil, err 55 | } 56 | return &pb.RmvUserResp{}, nil 57 | } 58 | 59 | // FndUsers implements part of the pb.UserServer interface. 60 | func (s *UserDB) FndUsers(ctx context.Context, req *pb.FndUsersReq) (*pb.UsersResp, error) { 61 | r := &pb.UsersResp{} 62 | if err := s.findUsers(ctx, req, r); err != nil { 63 | return nil, err 64 | } 65 | return r, nil 66 | } 67 | 68 | // AddRole implements part of the pb.UserServer interface. 69 | func (s *UserDB) AddRole(ctx context.Context, req *pb.AddRoleReq) (*pb.RoleResp, error) { 70 | r := &pb.RoleResp{} 71 | if err := s.upsertRole(ctx, "", req, r); err != nil { 72 | return nil, err 73 | } 74 | return r, nil 75 | } 76 | 77 | // FndRoles implements part of the pb.UserServer interface. 78 | func (s *UserDB) FndRoles(ctx context.Context, req *pb.FndRolesReq) (*pb.RolesResp, error) { 79 | r := &pb.RolesResp{} 80 | if err := s.findRoles(ctx, req, r); err != nil { 81 | return nil, err 82 | } 83 | return r, nil 84 | } 85 | -------------------------------------------------------------------------------- /docs/learning/go_cmd.md: -------------------------------------------------------------------------------- 1 | # Go and the Go Command Walk-through 2 | 3 | ## Introduction 4 | 5 | - [The Go Project](https://golang.org/project) 6 | 7 | ## Installation 8 | 9 | - [Download and Install](https://golang.org/doc/install) 10 | 11 | ## Operation 12 | 13 | ### The Basics 14 | 15 | - [Go Tour](https://tour.golang.org) 16 | - [Tutorial: Get started with Go](https://golang.org/doc/tutorial/getting-started) 17 | - [Tutorial: Create a Go Module](https://golang.org/doc/tutorial/create-module) 18 | 19 | ### A Little More 20 | 21 | - [Writing Web Applications](https://golang.org/doc/articles/wiki) 22 | - [How to Write Go Code](https://golang.org/doc/code) 23 | 24 | ### Specifics 25 | 26 | - [Command go](https://golang.org/cmd/go) 27 | 28 | #### > `go mod` 29 | 30 | Used to manage the module namespace of your project, as well as the list of 31 | modules that your module is dependent on. 32 | 33 | Start your project: 34 | 35 | ```sh 36 | go mod init github.com/OpenEugene/openboard 37 | ``` 38 | 39 | #### > `go get` and `mod` again 40 | 41 | Used to obtain code from a given module to be added to your own module. 42 | 43 | Add a dependency: 44 | 45 | ```sh 46 | go get github.com/codemodus/sigmon/v2@latest 47 | ``` 48 | 49 | If you've modified your project imports and wish to update your list of modules, 50 | `go mod` comes back into the picture. 51 | 52 | Ensure dependencies are recorded properly: 53 | 54 | ```sh 55 | go mod tidy 56 | ``` 57 | 58 | #### > `go test` 59 | 60 | Used to run tests using the standard library test framework. Files with the 61 | suffix `_test.go` will be picked up for handling. 62 | 63 | Run tests: 64 | 65 | ```sh 66 | go test # can be run with -v to see more info 67 | ``` 68 | 69 | #### > `go run` 70 | 71 | Used to build and run a temporary executable of your application. 72 | 73 | Build and run (temporary): 74 | 75 | ```sh 76 | go run . 77 | ``` 78 | 79 | #### > `go build` 80 | 81 | Used to build an executable of your application. 82 | 83 | Build and run (persistent): 84 | 85 | ```sh 86 | go build -o ./build/myproject 87 | ./myproject 88 | ``` 89 | 90 | #### > `go install` 91 | 92 | Used to install an executable directly from a repository. 93 | 94 | Install from source and run: 95 | 96 | ```sh 97 | go install github.com/OpenEugene/openboard/back/cmd/openbsrv@latest 98 | openbsrv 99 | ``` 100 | 101 | You will need to be sure that the install location is in your PATH environment 102 | variable. 103 | 104 | > Executables are installed in the directory named by the GOBIN environment 105 | > variable, which defaults to $GOPATH/bin or $HOME/go/bin if the GOPATH 106 | > environment variable is not set. Executables in $GOROOT are installed in 107 | > $GOROOT/bin or $GOTOOLDIR instead of $GOBIN. 108 | -------------------------------------------------------------------------------- /msgs/proto/post.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // openboard 4 | // 5 | // user semantics: 6 | // 7 | // Add{T}(s) (Add{T}(s)Req) returns {T}(s)Resp // POST 8 | // Ovr{T}(s) (Ovr{T}(s)Req) returns {T}(s)Resp // PUT 9 | // Mod{T}(s) (Mod{T}(s)Req) returns {T}(s)Resp // PATCH 10 | // Get{T} (Get{T}Req) returns {T}Resp // GET 11 | // Fnd{T}s (Fnd{T}sReq) returns {T}sResp // GET 12 | // Rmv{T}(s) (Rmv{T}(s)Req) returns EmptyResp // DELETE 13 | // Unr{T} (Unr{T}Req) returns {T}Resp // PATCH 14 | package pb; 15 | 16 | option go_package = ".;pb"; 17 | 18 | import "google/api/annotations.proto"; 19 | import "google/protobuf/timestamp.proto"; 20 | 21 | message CommentResp {} 22 | 23 | message AddCommentResp {} 24 | 25 | message TypeResp { 26 | string id = 1; 27 | string name = 2; 28 | } 29 | 30 | message AddTypeReq { string name = 1; } 31 | 32 | message TypesResp { 33 | repeated TypeResp items = 1; 34 | uint32 total = 2; 35 | } 36 | 37 | message FndTypesReq { 38 | uint32 limit = 1; 39 | uint32 lapse = 2; 40 | } 41 | 42 | message PostResp { 43 | string id = 1; 44 | string slug = 2; 45 | string title = 3; 46 | string body = 4; 47 | string typeId = 5; 48 | google.protobuf.Timestamp created = 6; 49 | google.protobuf.Timestamp updated = 7; 50 | google.protobuf.Timestamp deleted = 8; 51 | google.protobuf.Timestamp blocked = 9; 52 | } 53 | 54 | message AddPostReq { 55 | string title = 1; 56 | string body = 2; 57 | string typeId = 3; 58 | } 59 | 60 | message OvrPostReq { 61 | string id = 1; 62 | AddPostReq req = 2; 63 | } 64 | 65 | message PostsResp { 66 | repeated PostResp posts = 1; 67 | uint32 total = 2; 68 | } 69 | 70 | message FndPostsReq { 71 | repeated string keywords = 1; 72 | google.protobuf.Timestamp created = 2; 73 | google.protobuf.Timestamp updated = 3; 74 | google.protobuf.Timestamp deleted = 4; 75 | } 76 | 77 | message RmvPostReq { string id = 1; } 78 | 79 | message RmvPostResp {} 80 | 81 | service Post { 82 | rpc AddType(AddTypeReq) returns (TypeResp) { 83 | option (google.api.http) = { 84 | post : "/type" 85 | body : "*" 86 | }; 87 | } 88 | 89 | rpc FndTypes(FndTypesReq) returns (TypesResp) { 90 | option (google.api.http) = { 91 | get : "/types" 92 | }; 93 | } 94 | 95 | rpc AddPost(AddPostReq) returns (PostResp) { 96 | option (google.api.http) = { 97 | post : "/post" 98 | body : "*" 99 | }; 100 | }; 101 | 102 | rpc FndPosts(FndPostsReq) returns (PostsResp) { 103 | option (google.api.http) = { 104 | get : "/posts" 105 | }; 106 | } 107 | 108 | rpc OvrPost(OvrPostReq) returns (PostResp) { 109 | option (google.api.http) = { 110 | put : "/post/{id}" 111 | body : "*" 112 | }; 113 | } 114 | 115 | rpc RmvPost(RmvPostReq) returns (RmvPostResp) { 116 | option (google.api.http) = { 117 | delete : "/post/{id}" 118 | }; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /front/src/Proto/Auth.elm: -------------------------------------------------------------------------------- 1 | module Proto.Auth exposing (AddAuthReq, AddVoucherReq, AuthResp, RmvAuthReq, addAuthReqDecoder, addAuthReqEncoder, addVoucherReqDecoder, addVoucherReqEncoder, authRespDecoder, authRespEncoder, rmvAuthReqDecoder, rmvAuthReqEncoder) 2 | 3 | -- DO NOT EDIT 4 | -- AUTOGENERATED BY THE ELM PROTOCOL BUFFER COMPILER 5 | -- https://github.com/tiziano88/elm-protobuf 6 | -- source file: proto/auth.proto 7 | -- ALSO PROCESSED BY elm-format 0.8.1 8 | 9 | import Json.Decode as JD 10 | import Json.Encode as JE 11 | import Protobuf exposing (..) 12 | 13 | 14 | type alias AuthResp = 15 | { token : String -- 1 16 | } 17 | 18 | 19 | authRespDecoder : JD.Decoder AuthResp 20 | authRespDecoder = 21 | JD.lazy <| 22 | \_ -> 23 | decode AuthResp 24 | |> required "token" JD.string "" 25 | 26 | 27 | authRespEncoder : AuthResp -> JE.Value 28 | authRespEncoder v = 29 | JE.object <| 30 | List.filterMap identity <| 31 | [ requiredFieldEncoder "token" JE.string "" v.token 32 | ] 33 | 34 | 35 | type alias AddAuthReq = 36 | { username : String -- 1 37 | , password : String -- 2 38 | } 39 | 40 | 41 | addAuthReqDecoder : JD.Decoder AddAuthReq 42 | addAuthReqDecoder = 43 | JD.lazy <| 44 | \_ -> 45 | decode AddAuthReq 46 | |> required "username" JD.string "" 47 | |> required "password" JD.string "" 48 | 49 | 50 | addAuthReqEncoder : AddAuthReq -> JE.Value 51 | addAuthReqEncoder v = 52 | JE.object <| 53 | List.filterMap identity <| 54 | [ requiredFieldEncoder "username" JE.string "" v.username 55 | , requiredFieldEncoder "password" JE.string "" v.password 56 | ] 57 | 58 | 59 | type alias RmvAuthReq = 60 | { token : String -- 1 61 | } 62 | 63 | 64 | rmvAuthReqDecoder : JD.Decoder RmvAuthReq 65 | rmvAuthReqDecoder = 66 | JD.lazy <| 67 | \_ -> 68 | decode RmvAuthReq 69 | |> required "token" JD.string "" 70 | 71 | 72 | rmvAuthReqEncoder : RmvAuthReq -> JE.Value 73 | rmvAuthReqEncoder v = 74 | JE.object <| 75 | List.filterMap identity <| 76 | [ requiredFieldEncoder "token" JE.string "" v.token 77 | ] 78 | 79 | 80 | type alias AddVoucherReq = 81 | { notify : Bool -- 1 82 | , email : String -- 2 83 | , password : Bool -- 3 84 | } 85 | 86 | 87 | addVoucherReqDecoder : JD.Decoder AddVoucherReq 88 | addVoucherReqDecoder = 89 | JD.lazy <| 90 | \_ -> 91 | decode AddVoucherReq 92 | |> required "notify" JD.bool False 93 | |> required "email" JD.string "" 94 | |> required "password" JD.bool False 95 | 96 | 97 | addVoucherReqEncoder : AddVoucherReq -> JE.Value 98 | addVoucherReqEncoder v = 99 | JE.object <| 100 | List.filterMap identity <| 101 | [ requiredFieldEncoder "notify" JE.bool False v.notify 102 | , requiredFieldEncoder "email" JE.string "" v.email 103 | , requiredFieldEncoder "password" JE.bool False v.password 104 | ] 105 | -------------------------------------------------------------------------------- /back/cmd/openbsrv/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path" 8 | 9 | "github.com/codemodus/sigmon/v2" 10 | 11 | "github.com/OpenEugene/openboard/back/internal/dbg" 12 | "github.com/OpenEugene/openboard/back/internal/log" 13 | ) 14 | 15 | func main() { 16 | if err := run(); err != nil { 17 | cmd := path.Base(os.Args[0]) 18 | fmt.Fprintf(os.Stderr, "%s: %s\n", cmd, err) 19 | os.Exit(1) 20 | } 21 | } 22 | 23 | func run() error { 24 | var ( 25 | dbdrvr = "mysql" 26 | dbname = "openeug_openb_dev" 27 | dbuser = "openeug_openbdev" 28 | dbpass = "" 29 | dbaddr = "127.0.0.1" 30 | dbport = ":3306" 31 | migrate bool 32 | rollback bool 33 | skipsrv bool 34 | debug bool 35 | frontDir = "../../../front/public" 36 | migTblPfx = "mig_" 37 | ) 38 | 39 | flag.StringVar(&dbname, "dbname", dbname, "database name") 40 | flag.StringVar(&dbuser, "dbuser", dbuser, "database user") 41 | flag.StringVar(&dbpass, "dbpass", dbpass, "database pass") 42 | flag.StringVar(&dbaddr, "dbaddr", dbaddr, "database addr") 43 | flag.StringVar(&dbport, "dbport", dbport, "database port") 44 | flag.BoolVar(&migrate, "migrate", migrate, "migrate up") 45 | flag.BoolVar(&rollback, "rollback", rollback, "migrate dn") 46 | flag.BoolVar(&skipsrv, "skipsrv", skipsrv, "skip server run") 47 | flag.BoolVar(&debug, "debug", debug, "debug true or false") 48 | flag.StringVar(&frontDir, "frontdir", frontDir, "front public assets directory") 49 | flag.Parse() 50 | 51 | sm := sigmon.New(nil) 52 | sm.Start() 53 | defer sm.Stop() 54 | 55 | logCfg := log.Config{ 56 | Err: log.Output{ 57 | Out: os.Stdout, 58 | Prefix: "[ERROR] ", 59 | }, 60 | Inf: log.Output{ 61 | Out: os.Stdout, 62 | Prefix: "[INFO] ", 63 | }, 64 | } 65 | log := log.New(logCfg) 66 | 67 | if debug { 68 | dbg.SetOut(os.Stdout) 69 | } 70 | 71 | dbg.Logf("set up SQL database at %s:%s.", dbaddr, dbport) 72 | db, err := newSQLDB(dbdrvr, dbCreds(dbname, dbuser, dbpass, dbaddr, dbport)) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | mig, err := newDBMig(db, dbdrvr, migTblPfx) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | gsrv, err := newGRPCSrv(":4242", db, dbdrvr) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | mig.addMigrators(gsrv.services()...) 88 | if mres, migType := mig.run(migrate, rollback); len(migType) > 0 { 89 | if mres.HasError() { 90 | return mres.ErrsErr() 91 | } 92 | log.Info("%s: %s", migType, mres) 93 | } 94 | 95 | if skipsrv { 96 | fmt.Println("servers will not be run; exiting") 97 | return nil 98 | } 99 | 100 | hsrv, err := newHTTPSrv(":4242", ":4243", nil) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | fsrv, err := newFrontSrv(":4244", frontDir, nil) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | m := newServerMgmt(gsrv, hsrv, fsrv) 111 | 112 | sm.Set(func(s *sigmon.State) { 113 | if err := m.stop(); err != nil { 114 | fmt.Fprintln(os.Stderr, err) 115 | } 116 | }) 117 | 118 | log.Info("to gracefully stop the application, send signal like TERM (CTRL-C) or HUP") 119 | 120 | return m.serve() 121 | } 122 | -------------------------------------------------------------------------------- /back/internal/postsvc/postsvc.go: -------------------------------------------------------------------------------- 1 | package postsvc 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "io/fs" 7 | "io/ioutil" 8 | 9 | "github.com/OpenEugene/openboard/back/internal/pb" 10 | "github.com/OpenEugene/openboard/back/internal/postsvc/internal/asset" 11 | "github.com/OpenEugene/openboard/back/internal/postsvc/internal/postdb" 12 | 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | var _ pb.PostServer = &PostSvc{} 17 | 18 | // var _ grpcsrv.Registerable = &PostSvc{} 19 | // var _ sqlmig.DataProvider = &PostSvc{} 20 | // var _ sqlmig.Regularizer = &PostSvc{} 21 | 22 | type relDB interface { 23 | pb.PostServer 24 | } 25 | 26 | // PostSvc encapsulates dependencies and data required to implement the 27 | // pb.PostServer interface. 28 | type PostSvc struct { 29 | db relDB 30 | } 31 | 32 | // New returns a pointer to a PostSvc instance or an error. 33 | func New(relDb *sql.DB, driver string, offset uint64) (*PostSvc, error) { 34 | db, err := postdb.New(relDb, driver, offset) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | s := PostSvc{ 40 | db: db, 41 | } 42 | 43 | return &s, nil 44 | } 45 | 46 | // AddType implements part of the pb.PostServer interface. 47 | func (s *PostSvc) AddType(ctx context.Context, req *pb.AddTypeReq) (*pb.TypeResp, error) { 48 | return s.db.AddType(ctx, req) 49 | } 50 | 51 | // FndTypes implements part of the pb.PostServer interface. 52 | func (s *PostSvc) FndTypes(ctx context.Context, req *pb.FndTypesReq) (*pb.TypesResp, error) { 53 | return s.db.FndTypes(ctx, req) 54 | } 55 | 56 | // AddPost implements part of the pb.PostServer interface. 57 | func (s *PostSvc) AddPost(ctx context.Context, req *pb.AddPostReq) (*pb.PostResp, error) { 58 | return s.db.AddPost(ctx, req) 59 | } 60 | 61 | // FndPosts implements part of the pb.PostServer interface. 62 | func (s *PostSvc) FndPosts(ctx context.Context, req *pb.FndPostsReq) (*pb.PostsResp, error) { 63 | return s.db.FndPosts(ctx, req) 64 | } 65 | 66 | // OvrPost implements part of the pb.PostServer interface. 67 | func (s *PostSvc) OvrPost(ctx context.Context, req *pb.OvrPostReq) (*pb.PostResp, error) { 68 | return s.db.OvrPost(ctx, req) 69 | } 70 | 71 | // RmvPost implements part of the pb.PostServer interface. 72 | func (s *PostSvc) RmvPost(ctx context.Context, req *pb.RmvPostReq) (*pb.RmvPostResp, error) { 73 | return s.db.RmvPost(ctx, req) 74 | } 75 | 76 | // RegisterWithGRPCServer implements the grpcsrv.Registerable interface. 77 | func (s *PostSvc) RegisterWithGRPCServer(g *grpc.Server) error { 78 | pb.RegisterPostServer(g, s) 79 | return nil 80 | } 81 | 82 | // MigrationData ... 83 | func (s *PostSvc) MigrationData() (string, map[string][]byte) { 84 | name := "postsvc" 85 | m := make(map[string][]byte) 86 | 87 | afs, err := asset.NewFS() 88 | if err != nil { 89 | return name, nil 90 | } 91 | 92 | sqls, err := fs.Glob(afs, "*.sql") 93 | if err != nil { 94 | return name, nil 95 | } 96 | 97 | for _, sql := range sqls { 98 | f, err := afs.Open(sql) 99 | if err != nil { 100 | return name, nil 101 | } 102 | 103 | m[sql], err = ioutil.ReadAll(f) 104 | if err != nil { 105 | return name, nil 106 | } 107 | } 108 | 109 | return name, m 110 | } 111 | 112 | // Regularize ... 113 | func (s *PostSvc) Regularize(ctx context.Context) error { 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /back/internal/usersvc/usersvc.go: -------------------------------------------------------------------------------- 1 | package usersvc 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "io/fs" 7 | "io/ioutil" 8 | 9 | "github.com/OpenEugene/openboard/back/internal/pb" 10 | "github.com/OpenEugene/openboard/back/internal/usersvc/internal/asset" 11 | "github.com/OpenEugene/openboard/back/internal/usersvc/internal/userdb" 12 | "google.golang.org/grpc" 13 | ) 14 | 15 | var _ pb.UserSvcServer = &UserSvc{} 16 | 17 | //var _ grpcsrv.Registerable = &UserSvc{} 18 | //var _ sqlmig.DataProvider = &UserSvc{} 19 | //var _ sqlmig.Regularizer = &UserSvc{} 20 | 21 | type relDb interface { 22 | pb.UserSvcServer 23 | } 24 | 25 | // UserSvc encapsulates dependencies and data required to implement the 26 | // pb.UserServer interface. 27 | type UserSvc struct { 28 | db relDb 29 | } 30 | 31 | // New returns a pointer to a UserSvc instance or an error. 32 | func New(relDb *sql.DB, driver string, offset uint64) (*UserSvc, error) { 33 | db, err := userdb.New(relDb, driver, offset) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | s := UserSvc{ 39 | db: db, 40 | } 41 | 42 | return &s, nil 43 | } 44 | 45 | // RegisterWithGRPCServer implements the grpcsrv.Registerable interface. 46 | func (s *UserSvc) RegisterWithGRPCServer(g *grpc.Server) error { 47 | pb.RegisterUserSvcServer(g, s) 48 | 49 | return nil 50 | } 51 | 52 | // AddRole implements part of the pb.UserServer interface. 53 | func (s *UserSvc) AddRole(ctx context.Context, req *pb.AddRoleReq) (*pb.RoleResp, error) { 54 | return s.db.AddRole(ctx, req) 55 | } 56 | 57 | // FndRoles implements part of the pb.UserServer interface. 58 | func (s *UserSvc) FndRoles(ctx context.Context, req *pb.FndRolesReq) (*pb.RolesResp, error) { 59 | return s.db.FndRoles(ctx, req) 60 | } 61 | 62 | // AddUser implements part of the pb.UserServer interface. 63 | func (s *UserSvc) AddUser(ctx context.Context, req *pb.AddUserReq) (*pb.UserResp, error) { 64 | return s.db.AddUser(ctx, req) 65 | } 66 | 67 | // OvrUser implements part of the pb.UserServer interface. 68 | func (s *UserSvc) OvrUser(ctx context.Context, req *pb.OvrUserReq) (*pb.UserResp, error) { 69 | return s.db.OvrUser(ctx, req) 70 | } 71 | 72 | // FndUsers implements part of the pb.UserServer interface. 73 | func (s *UserSvc) FndUsers(ctx context.Context, req *pb.FndUsersReq) (*pb.UsersResp, error) { 74 | return s.db.FndUsers(ctx, req) 75 | } 76 | 77 | // RmvUser implements part of the pb.UserServer interface. 78 | func (s *UserSvc) RmvUser(ctx context.Context, req *pb.RmvUserReq) (*pb.RmvUserResp, error) { 79 | return s.db.RmvUser(ctx, req) 80 | } 81 | 82 | // MigrationData ... 83 | func (s *UserSvc) MigrationData() (string, map[string][]byte) { 84 | name := "usersvc" 85 | m := make(map[string][]byte) 86 | 87 | afs, err := asset.NewFS() 88 | if err != nil { 89 | return name, nil 90 | } 91 | 92 | sqls, err := fs.Glob(afs, "*.sql") 93 | if err != nil { 94 | return name, nil 95 | } 96 | 97 | for _, sql := range sqls { 98 | f, err := afs.Open(sql) 99 | if err != nil { 100 | return name, nil 101 | } 102 | 103 | m[sql], err = ioutil.ReadAll(f) 104 | if err != nil { 105 | return name, nil 106 | } 107 | } 108 | 109 | return name, m 110 | } 111 | 112 | //Regularize ... 113 | func (s *UserSvc) Regularize(ctx context.Context) error { 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /docs/learning/git.md: -------------------------------------------------------------------------------- 1 | # Git Walk-through 2 | 3 | ## Introduction 4 | 5 | - [Programming with Mosh - What is Git?](https://youtu.be/2ReR1YJrNOM) 6 | - [Paul Programming - What is Git?](https://youtu.be/OqmSzXDrJBk) 7 | 8 | The first video is "Git in principle", and the second is "Git in practice". 9 | 10 | The second video seems to imply that Git is centralized, which is in opposition 11 | to the description given in the first video. Centralized version control tools 12 | only permit one user to handle a portion of code at a time. Git is a distributed 13 | version control tool. Many users can operate on a Git project simultaneously, 14 | and the resulting changes are brought together into one place. 15 | 16 | ## Installation 17 | 18 | - [Git - Getting Started](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 19 | 20 | ## Configuration 21 | 22 | - [Git - Customizing Git](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) 23 | 24 | The most pertinent information is in the "Git Configuration" section only. Feel 25 | free to read further for more details and advanced configuration. The email and 26 | username you select will show up in publicly available git commit histories. 27 | 28 | ## Operation 29 | 30 | - [Udacity - Git Workflow](https://youtu.be/3a2x1iJFJWc) 31 | - [Kev The Dev - Easy Overview](https://youtu.be/7dYHRI55wxo) 32 | 33 | In the Udacity video, four columns are shown. It may help to think 34 | of the column on the far left as the newest changes to the code. We make changes 35 | to code, then pass those changes through certain steps to ensure the difference 36 | the changes make is correct and understandable. 37 | 38 | Kev's overview has some caveats. First, when cloning a project, if you have not 39 | first setup SSH access to your repository, use HTTPS instead (seen at 6:54). 40 | Second, when you see the usage of `git checkout` as it applies to switching 41 | branches, it is preferrable to use the `git switch` command. So, `git checkout 42 | -b new-branch` should be `git switch -c new-branch`, and `git checkout 43 | existing-branch` should be `git switch existing-branch`. The usage of checkout 44 | to move to a specific commit (seen at 4:50) is correct. The git command will 45 | give notices to use `git switch` in the relevant cases and those messages can be 46 | seen in the video. Lastly, it is currently preferred to use the default branch 47 | name "main" rather than "master". 48 | 49 | ## Interpretation 50 | 51 | A simple list of commands to start work on an issue: 52 | ```sh 53 | git switch main 54 | git pull origin main 55 | git switch -c my_branch-123 56 | # make change(s) 57 | git add . 58 | git commit -m"Make specific change" 59 | git push -u origin my_branch-123 60 | ``` 61 | 62 | Additional commits can be added: 63 | ```sh 64 | git add . 65 | git commit -m"Fix some mistake" 66 | git push 67 | ``` 68 | 69 | It may be useful to run `git remote prune origin` after `git pull origin main` 70 | to ensure that the references to remote branches are cleaned up. 71 | 72 | ## Visualization 73 | 74 | - [Git GUIs](https://git-scm.com/downloads/guis) 75 | 76 | Git, as a command line interface (CLI) tool, is easy to interact with for those 77 | who are comfortable working from a terminal. However, it can be useful, not 78 | just for learners, to have clear visual represenations of the data being 79 | operated on. Many graphical user interface (GUI) programs are available that 80 | leverage the Git CLI as their "engine". 81 | -------------------------------------------------------------------------------- /msgs/proto/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // openboard 4 | // 5 | // user semantics: 6 | // 7 | // Add{T}(s) (Add{T}(s)Req) returns {T}(s)Resp // POST 8 | // Ovr{T}(s) (Ovr{T}(s)Req) returns {T}(s)Resp // PUT 9 | // Mod{T}(s) (Mod{T}(s)Req) returns {T}(s)Resp // PATCH 10 | // Get{T} (Get{T}Req) returns {T}Resp // GET 11 | // Fnd{T}s (Fnd{T}sReq) returns {T}sResp // GET 12 | // Rmv{T}(s) (Rmv{T}(s)Req) returns EmptyResp // DELETE 13 | // Unr{T} (Unr{T}Req) returns {T}Resp // PATCH 14 | package pb; 15 | 16 | option go_package = ".;pb"; 17 | 18 | import "google/api/annotations.proto"; 19 | import "google/protobuf/timestamp.proto"; 20 | 21 | message RoleResp { 22 | string id = 1; 23 | string name = 2; 24 | } 25 | 26 | message AddRoleReq { string name = 1; } 27 | 28 | message RolesResp { 29 | repeated RoleResp items = 1; 30 | uint32 total = 2; 31 | } 32 | 33 | message FndRolesReq { 34 | repeated string roleIds = 1; 35 | repeated string roleNames = 2; 36 | uint32 limit = 3; 37 | uint32 lapse = 4; 38 | } 39 | 40 | message User { 41 | string id = 1; 42 | string username = 2; 43 | string email = 3; 44 | bool emailHold = 4; 45 | string altmail = 5; 46 | bool altmailHold = 6; 47 | string fullName = 7; 48 | string avatar = 8; 49 | repeated RoleResp roles = 9; 50 | google.protobuf.Timestamp lastLogin = 10; 51 | google.protobuf.Timestamp created = 11; 52 | google.protobuf.Timestamp updated = 12; 53 | google.protobuf.Timestamp deleted = 13; 54 | google.protobuf.Timestamp blocked = 14; 55 | } 56 | 57 | message UserResp { User item = 1; } 58 | 59 | message AddUserReq { 60 | string username = 1; 61 | string email = 2; 62 | bool emailHold = 3; 63 | string altmail = 4; 64 | bool altmailHold = 5; 65 | string fullName = 6; 66 | string avatar = 7; 67 | string password = 8; 68 | repeated string roleIds = 9; 69 | } 70 | 71 | message OvrUserReq { 72 | string id = 1; 73 | AddUserReq req = 2; 74 | } 75 | 76 | message UsersResp { 77 | repeated User items = 1; 78 | uint32 total = 2; 79 | } 80 | 81 | message FndUsersReq { 82 | repeated string roleIds = 1; 83 | string email = 2; 84 | bool emailHold = 3; 85 | string altmail = 4; 86 | bool altmailHold = 5; 87 | google.protobuf.Timestamp createdFirst = 6; 88 | google.protobuf.Timestamp createdFinal = 7; 89 | bool createdDesc = 8; 90 | uint32 limit = 9; 91 | uint32 lapse = 10; 92 | } 93 | 94 | message RmvUserResp {} 95 | 96 | message RmvUserReq { string id = 1; } 97 | 98 | service UserSvc { 99 | rpc AddRole(AddRoleReq) returns (RoleResp) {} 100 | 101 | rpc FndRoles(FndRolesReq) returns (RolesResp) { 102 | option (google.api.http) = { 103 | get : "/roles" 104 | }; 105 | } 106 | 107 | rpc AddUser(AddUserReq) returns (UserResp) { 108 | option (google.api.http) = { 109 | post : "/user" 110 | body : "*" 111 | }; 112 | } 113 | 114 | rpc OvrUser(OvrUserReq) returns (UserResp) { 115 | option (google.api.http) = { 116 | put : "/user/{id}" 117 | body : "*" 118 | }; 119 | } 120 | 121 | rpc FndUsers(FndUsersReq) returns (UsersResp) { 122 | option (google.api.http) = { 123 | get : "/users" 124 | }; 125 | } 126 | 127 | rpc RmvUser(RmvUserReq) returns (RmvUserResp) { 128 | option (google.api.http) = { 129 | delete : "/user/{id}" 130 | }; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Welcome 2 | 3 | ### Please & Thank You 4 | 5 | First off, thank you for considering contributing to Openboard. It's people 6 | like you that make projects like this possible! 7 | 8 | Following this guide helps to communicate that you respect the time of the 9 | developers managing and developing this open source project. In return, they 10 | should reciprocate that respect in addressing your issue, assessing changes, 11 | and helping you finalize your pull requests. 12 | 13 | ## Accountability 14 | 15 | Openboard is a project utilizing and teaching [Go](https://golang.org) and 16 | [Elm](https://elm-lang.org). This project is part of the 17 | [OpenEugene](http://openeugene.org) collective, and our meetings are part of 18 | the [EugeneTech](https://eugenetech.org) community. Therefore, we align 19 | ourselves with the codes of conduct put forth by those orgs/communities. 20 | 21 | - [EugeneTech Policies](https://github.com/EugTech/policies) 22 | - [OpenEugene Code of Conduct (via CFA)](https://brigade.codeforamerica.org/about/code-of-conduct) 23 | - [Go Community Code of Conduct](https://golang.org/conduct) 24 | - [Elm Community Spaces Code of Conduct](https://github.com/elm-community/discussions/blob/master/code-of-conduct.md) 25 | 26 | ## Getting Started (Tools and Building) 27 | 28 | - [Openboard Readme](../README.md) 29 | 30 | ## Contributing 31 | 32 | ### General 33 | 34 | We would appreciate that most contributions to this project be made by attending 35 | our sprint wrap & plan and/or check-ins. See the 36 | [README](../README.md) for more 37 | information. Contributing from forks has not yet been tested regarding our 38 | automated tracking mechanisms. 39 | 40 | ### Reporting 41 | 42 | - "Issues" contains requested corrections and improvements. 43 | - "Pull Requests" contains mutations that each directly resolve "Issues" 44 | 45 | ### Tracking 46 | 47 | Issues and Pull Requests are tracked using Kanban boards. Board automation is 48 | provided in both cases. 49 | 50 | #### [Issue Qualification](https://github.com/OpenEugene/openboard/projects/3) 51 | 52 | When an Issue is: 53 | - up for consideration, it is added to the "Proposed" column 54 | - accepted to be resolved, it is moved to the "Accepted" column 55 | - actively being resolved, it is moved to the "Claimed" column 56 | - fully resolved, it is moved to the "Resolved" column 57 | 58 | *Most of these steps are automated. The only step that is often performed 59 | manually is the first step where an issue is added to the "Proposed" column. 60 | 61 | #### [Change Management](https://github.com/OpenEugene/openboard/projects/1) 62 | 63 | When a PR is: 64 | - created, it is added to the "Initiated" column 65 | - ready for review, it is added to the "Submitted" column 66 | - closed, it is added to the "Finished" column 67 | 68 | *All of these steps are automated. 69 | 70 | ### Automation 71 | 72 | #### Pull Request Creation 73 | A Branch that has the suffix "-{issue_number}" (e.g. "my_branch-123") and is 74 | pushed to the project will automatically have a PR opened that is associated 75 | with the relevant Issue. The PR title is derived from the Issue title. The PR is 76 | automatically added to the "Change Management" project. 77 | 78 | #### WIP Convenience 79 | A Branch that contains a commit message that has the prefix "WIP" will have its 80 | PR title prefixed with "WIP: " (e.g. "WIP: Fix some bug"). A Branch that 81 | contains a commit message that has the prefix "NOWIP" and also has an existing 82 | PR tracking it will cause the PR title "WIP: " prefix to be trimmed. 83 | -------------------------------------------------------------------------------- /back/README.md: -------------------------------------------------------------------------------- 1 | # openboard/back 2 | 3 | ## Getting Started 4 | 5 | ### Easy Mode 6 | 7 | The following scripts should be working for Bash on Linux, WSL, and Darwin. When 8 | installing the database, the suggested defaults for this project are: 9 | dbname = "openeug_openb_dev", and dbuser = "openeug_openbdev". 10 | 11 | ```sh 12 | # from {project_root} 13 | cd back/tools 14 | ./install-go 15 | ./install-tools 16 | ./install-mariadb # container-based alternative below 17 | ``` 18 | 19 | ```sh 20 | # database container setup (skip if using the "./install-mariadb" script above) 21 | # installs docker and docker-compose 22 | # from {project_root} 23 | cd back/tools/iso/ 24 | ./dev up # subcommands [up|dn|ip|clean] (default: up) 25 | ``` 26 | 27 | ### Normal Mode 28 | 29 | - [Install Go](https://golang.org/doc/install) 30 | - [Install Tools](./tools/install-tools) 31 | - [Install MariaDB (>=10.2.22)](https://www.google.com/search?q=install+mariadb+stable+on) 32 | 33 | ### Advanced Options 34 | 35 | #### Protobuf/protoc 36 | 37 | Protocol buffers are Google's language-neutral, platform-neutral, extensible 38 | mechanism for serializing structured data. 39 | 40 | [Learn More](https://developers.google.com/protocol-buffers/) | 41 | [Releases](https://github.com/protocolbuffers/protobuf/releases) | 42 | [Install From 43 | Source](https://github.com/protocolbuffers/protobuf/blob/master/src/README.md) 44 | 45 | ```sh 46 | # may require changes for your OS/ENV 47 | cd {your_source_code_dir} 48 | mkdir -p github.com/protocolbuffers/protobuf 49 | cd github.com/protocolbuffers/protobuf 50 | git clone https://github.com/protocolbuffers/protobuf.git . 51 | git submodule update --init --recursive 52 | ./autogen.sh 53 | ./configure 54 | make 55 | make check 56 | sudo make install 57 | sudo ldconfig # refresh shared library cache. 58 | ``` 59 | 60 | ## Project Source 61 | 62 | `openbsrv` will open three ports (4242, 4243, 4244). 4242 is used to serve gRPC 63 | endpoints. gRPC endpoints can be accessed directly, or via an HTTP gateway on 64 | port 4243. In order to view the API endpoints, please visit 65 | http://localhost:4243/v/docs. The frontend assets are served on port 4244. 66 | 67 | ### Clone 68 | 69 | ```sh 70 | cd {your_source_code_dir} 71 | mkdir -p github.com/OpenEugene/openboard 72 | cd github.com/OpenEugene/openboard 73 | git clone https://github.com/OpenEugene/openboard . 74 | ``` 75 | 76 | ### Build and Execute 77 | 78 | ```sh 79 | # from {project_root} 80 | cd back/cmd/openbsrv 81 | go build 82 | ./openbsrv --dbpass={your_dbpass} --migrate 83 | # be careful not to git add/commit the executable 84 | ``` 85 | 86 | Please refer to the [openbsrv readme](./cmd/openbsrv/README.md) for more details 87 | about usage and flags (e.g. -dbname, -dbuser, etc.). 88 | 89 | ### Database Migration Management 90 | 91 | ```sh 92 | # from {project_root} 93 | cd back/cmd/openbsrv 94 | ./openbsrv --dbpass={your_dbpass} --rollback 95 | ^C # Ctrl-C will send the system signal "SIGINT" and halts the program 96 | ./openbsrv --dbpass={your_dbpass} --migrate 97 | ``` 98 | 99 | ### Run Tests 100 | 101 | #### Unit Tests 102 | 103 | None at this time. 104 | 105 | #### Surface Tests 106 | 107 | None at this time. 108 | 109 | #### End-to-end Tests 110 | 111 | A convenience script has been provided to build and run the executable, and also 112 | run the end-to-end tests. 113 | 114 | ```sh 115 | # from {project_root} 116 | cd back/tests/openbsrv 117 | ./run-tests 118 | ``` 119 | 120 | ## Contributing 121 | 122 | - [Start here](../docs/CONTRIBUTING.md) 123 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /msgs/proto/include/googleapis/google/rpc/status.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.rpc; 18 | 19 | import "google/protobuf/any.proto"; 20 | 21 | option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; 22 | option java_multiple_files = true; 23 | option java_outer_classname = "StatusProto"; 24 | option java_package = "com.google.rpc"; 25 | option objc_class_prefix = "RPC"; 26 | 27 | 28 | // The `Status` type defines a logical error model that is suitable for different 29 | // programming environments, including REST APIs and RPC APIs. It is used by 30 | // [gRPC](https://github.com/grpc). The error model is designed to be: 31 | // 32 | // - Simple to use and understand for most users 33 | // - Flexible enough to meet unexpected needs 34 | // 35 | // # Overview 36 | // 37 | // The `Status` message contains three pieces of data: error code, error message, 38 | // and error details. The error code should be an enum value of 39 | // [google.rpc.Code][google.rpc.Code], but it may accept additional error codes if needed. The 40 | // error message should be a developer-facing English message that helps 41 | // developers *understand* and *resolve* the error. If a localized user-facing 42 | // error message is needed, put the localized message in the error details or 43 | // localize it in the client. The optional error details may contain arbitrary 44 | // information about the error. There is a predefined set of error detail types 45 | // in the package `google.rpc` that can be used for common error conditions. 46 | // 47 | // # Language mapping 48 | // 49 | // The `Status` message is the logical representation of the error model, but it 50 | // is not necessarily the actual wire format. When the `Status` message is 51 | // exposed in different client libraries and different wire protocols, it can be 52 | // mapped differently. For example, it will likely be mapped to some exceptions 53 | // in Java, but more likely mapped to some error codes in C. 54 | // 55 | // # Other uses 56 | // 57 | // The error model and the `Status` message can be used in a variety of 58 | // environments, either with or without APIs, to provide a 59 | // consistent developer experience across different environments. 60 | // 61 | // Example uses of this error model include: 62 | // 63 | // - Partial errors. If a service needs to return partial errors to the client, 64 | // it may embed the `Status` in the normal response to indicate the partial 65 | // errors. 66 | // 67 | // - Workflow errors. A typical workflow has multiple steps. Each step may 68 | // have a `Status` message for error reporting. 69 | // 70 | // - Batch operations. If a client uses batch request and batch response, the 71 | // `Status` message should be used directly inside batch response, one for 72 | // each error sub-response. 73 | // 74 | // - Asynchronous operations. If an API call embeds asynchronous operation 75 | // results in its response, the status of those operations should be 76 | // represented directly using the `Status` message. 77 | // 78 | // - Logging. If some API errors are stored in logs, the message `Status` could 79 | // be used directly after any stripping needed for security/privacy reasons. 80 | message Status { 81 | // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. 82 | int32 code = 1; 83 | 84 | // A developer-facing error message, which should be in English. Any 85 | // user-facing error message should be localized and sent in the 86 | // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. 87 | string message = 2; 88 | 89 | // A list of messages that carry the error details. There is a common set of 90 | // message types for APIs to use. 91 | repeated google.protobuf.Any details = 3; 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/wip.yml: -------------------------------------------------------------------------------- 1 | name: Manage WIP workflow 2 | on: 3 | push: 4 | branches: 5 | - '**-[0-9]+' 6 | jobs: 7 | build: 8 | name: WIP it 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Set branch name envvar 12 | run: | 13 | echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV 14 | 15 | - name: Set branch issue envvar 16 | run: | 17 | issue="${BRANCH_NAME##*-}" 18 | ! [[ "${issue}" =~ ^[0-9]+$ ]] && >&2 echo "issue num not parsed" && exit 1 19 | echo "BRANCH_ISSUE=${issue}" >> $GITHUB_ENV 20 | 21 | - uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 128 24 | ref: main 25 | 26 | - uses: actions/checkout@v2 27 | with: 28 | fetch-depth: 0 29 | ref: ${{ github.sha }} 30 | 31 | - name: Setup/update WIP pull requests and related issues 32 | run: | 33 | set -e 34 | 35 | echo "branch issue: ${BRANCH_ISSUE}" # debug 36 | 37 | infoFile="commits_info" 38 | git log \ 39 | --left-right \ 40 | --graph \ 41 | --cherry-pick \ 42 | --oneline \ 43 | --pretty=format:%s \ 44 | origin/main..origin/"${BRANCH_NAME}" >> "${infoFile}" 45 | echo >> "${infoFile}" # add missing newline 46 | 47 | tac "${infoFile}" > "rev_${infoFile}" && mv "rev_${infoFile}" "${infoFile}" # reverse data 48 | cat "${infoFile}" # debug 49 | 50 | wipLineNum="$(cat "${infoFile}" | grep "^> WIP" -n | tail -n 1 | cut -d: -f1)" 51 | nowipLineNum="$(cat "${infoFile}" | grep "^> NOWIP" -n | tail -n 1 | cut -d: -f1)" 52 | prList="$(hub pr list --head "${BRANCH_NAME}" --state=open --format="%i %t\n")" 53 | prCt="$(echo -ne "${prList}" | wc -l)" 54 | 55 | echo "wip:${wipLineNum} nowip:${nowipLineNum}" # debug 56 | echo pr list... # debug 57 | echo -ne "${prList}" # debug 58 | 59 | echo "${prCt} existing pr(s) for the branch issue number ${BRANCH_ISSUE}" # debug 60 | 61 | case "${prCt}" in 62 | 0) 63 | prTitle="$(hub issue show --format="%t" "${BRANCH_ISSUE}")" 64 | if [[ -n "${wipLineNum}" && "${nowipLineNum}" -lt "${wipLineNum}" ]]; then 65 | prTitle="WIP: ${prTitle}" 66 | fi 67 | 68 | echo "pr title: ${prTitle}" # debug 69 | 70 | hub pull-request \ 71 | --assign "${GITHUB_ACTOR}" \ 72 | --base main \ 73 | --head ${BRANCH_NAME} \ 74 | --message "${prTitle}" \ 75 | --message "Closes #${BRANCH_ISSUE}" 76 | 77 | echo "github actor: ${GITHUB_ACTOR}" # debug 78 | 79 | hub issue update "${BRANCH_ISSUE}" \ 80 | -a "${GITHUB_ACTOR}" 81 | ;; 82 | 83 | 1) 84 | prTitle="$(echo -ne "${prList}" | cut -d" " -f2-)" 85 | prIssue="$(echo -ne "${prList}" | grep -o "#[0-9]*" | tr -d "#")" 86 | shouldChange=false 87 | 88 | if [[ -n "${nowipLineNum}" && "${nowipLineNum}" -gt "${wipLineNum}" ]]; then 89 | if [[ "${prTitle}" =~ ^WIP: ]]; then 90 | shouldChange=true 91 | prTitle="${prTitle#WIP:}" 92 | prTitle="${prTitle# }" 93 | fi 94 | elif [[ -n "${wipLineNum}" && "${nowipLineNum}" -lt "${wipLineNum}" ]]; then 95 | prTitle="$(echo -ne "${prList}" | cut -d" " -f2-)" 96 | if [[ ! "${prTitle}" =~ ^WIP: ]]; then 97 | shouldChange=true 98 | prTitle="WIP: ${prTitle}" 99 | fi 100 | fi 101 | 102 | if ${shouldChange}; then 103 | echo "shouldChange: ${shouldChange}" # debug 104 | 105 | prBody="$(hub pr list --head "${BRANCH_NAME}" --state=open --format="%b")" 106 | 107 | echo "pr title: ${prTitle}" # debug 108 | echo "pr body: ${prBody}" # debug 109 | 110 | hub issue update "${prIssue}" \ 111 | --message "${prTitle}" \ 112 | --message "${prBody}" 113 | fi 114 | ;; 115 | 116 | *) 117 | echo "multiple PRs were found based on the same branch" 118 | echo "taking no action" 119 | ;; 120 | esac 121 | env: 122 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 123 | -------------------------------------------------------------------------------- /back/internal/postsvc/internal/postdb/qrypost.go: -------------------------------------------------------------------------------- 1 | package postdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/OpenEugene/openboard/back/internal/altr" 10 | "github.com/OpenEugene/openboard/back/internal/pb" 11 | "github.com/codemodus/uidgen" 12 | "github.com/go-sql-driver/mysql" 13 | ) 14 | 15 | type cx = context.Context 16 | 17 | var ( 18 | csvStr = altr.CSVFromStrings 19 | lim = altr.LimitUint32 20 | asTS = altr.Timestamp 21 | ) 22 | 23 | func parseOrUID(ug *uidgen.UIDGen, sid string) (uidgen.UID, bool) { 24 | if sid == "" { 25 | return ug.UID(), true 26 | } 27 | return ug.Parse(sid) 28 | } 29 | 30 | func (s *PostDB) upsertType(ctx cx, sid string, x *pb.AddTypeReq, y *pb.TypeResp) error { 31 | id, ok := parseOrUID(s.ug, sid) 32 | if !ok { 33 | return fmt.Errorf("invalid uid") 34 | } 35 | 36 | stmt, err := s.db.Prepare("INSERT INTO `type` (type_id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE type_id = ?, name = ?") 37 | if err != nil { 38 | return err 39 | } 40 | 41 | _, err = stmt.Exec(&id, x.Name, &id, x.Name) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | y.Id = id.String() 47 | y.Name = x.Name 48 | 49 | return nil 50 | } 51 | 52 | func (s *PostDB) findTypes(ctx cx, x *pb.FndTypesReq, y *pb.TypesResp) error { 53 | selStmt, err := s.db.Prepare("SELECT type_id, name FROM type LIMIT ? OFFSET ?") 54 | if err != nil { 55 | return err 56 | } 57 | defer selStmt.Close() 58 | 59 | rows, err := selStmt.Query(x.Limit, x.Lapse) 60 | if err != nil { 61 | return err 62 | } 63 | defer rows.Close() 64 | 65 | for rows.Next() { 66 | r := pb.TypeResp{} 67 | 68 | err := rows.Scan(&r.Id, &r.Name) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | y.Items = append(y.Items, &r) 74 | } 75 | 76 | if err = rows.Err(); err != nil { 77 | return err 78 | } 79 | 80 | err = s.db.QueryRow("SELECT COUNT(*) FROM type").Scan(&y.Total) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (s *PostDB) upsertPost(ctx cx, sid string, x *pb.AddPostReq, y *pb.PostResp) error { 89 | id, ok := parseOrUID(s.ug, sid) 90 | if !ok { 91 | return fmt.Errorf("invalid uid") 92 | } 93 | 94 | stmt, err := s.db.Prepare("INSERT INTO post (post_id, type_id, slug, title, body) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE post_id = ?, type_id = ?, slug = ?, title = ?, body = ?") 95 | if err != nil { 96 | return err 97 | } 98 | 99 | _, err = stmt.Exec(&id, x.TypeId, "", x.Title, x.Body, &id, x.TypeId, "", x.Title, x.Body) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | y.Id = id.String() 105 | y.TypeId = x.TypeId 106 | y.Title = x.Title 107 | y.Body = x.Body 108 | 109 | return nil 110 | } 111 | 112 | func (s *PostDB) findPosts(ctx cx, x *pb.FndPostsReq, y *pb.PostsResp) error { 113 | selStmt, err := s.db.Prepare(` 114 | SELECT post_id, type_id, slug, title, body, created_at, updated_at, deleted_at 115 | FROM post 116 | WHERE (MATCH body AGAINST (? IN NATURAL LANGUAGE MODE)) OR 117 | (MATCH title AGAINST (? IN NATURAL LANGUAGE MODE)) OR 118 | body LIKE ? OR title LIKE ? 119 | ORDER BY 120 | (MATCH body AGAINST (? IN NATURAL LANGUAGE MODE))+(MATCH body AGAINST (? IN NATURAL LANGUAGE MODE)) 121 | DESC`) 122 | if err != nil { 123 | return err 124 | } 125 | defer selStmt.Close() 126 | 127 | like := strings.Join(x.Keywords, " ") 128 | 129 | rows, err := selStmt.Query( 130 | like, 131 | like, 132 | "%"+like+"%", 133 | "%"+like+"%", 134 | like, 135 | like, 136 | ) 137 | if err != nil { 138 | return err 139 | } 140 | defer rows.Close() 141 | 142 | for rows.Next() { 143 | r := pb.PostResp{} 144 | var tc, tu, td, tb mysql.NullTime 145 | 146 | err := rows.Scan(&r.Id, &r.TypeId, &r.Slug, &r.Title, &r.Body, &tc, &tu, &td) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | r.Created = asTS(tc.Time, tc.Valid) 152 | r.Updated = asTS(tu.Time, tu.Valid) 153 | r.Deleted = asTS(td.Time, td.Valid) 154 | r.Blocked = asTS(tb.Time, tb.Valid) 155 | 156 | y.Posts = append(y.Posts, &r) 157 | } 158 | 159 | if err = rows.Err(); err != nil { 160 | return err 161 | } 162 | 163 | err = s.db.QueryRow( 164 | "SELECT COUNT(*) FROM post WHERE title LIKE ? OR body LIKE ?", 165 | "%"+like+"%", 166 | "%"+like+"%", 167 | ).Scan(&y.Total) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | return nil 173 | } 174 | 175 | func (s *PostDB) deletePost(ctx cx, sid string) error { 176 | _, err := s.db.Exec( 177 | "UPDATE post SET deleted_at = ? WHERE post_id = ?", 178 | time.Now(), 179 | sid, 180 | ) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | return nil 186 | } 187 | -------------------------------------------------------------------------------- /back/internal/httpsrv/httpsrv.go: -------------------------------------------------------------------------------- 1 | package httpsrv 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io/ioutil" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/OpenEugene/openboard/back/internal/httpsrv/internal/asset" 12 | "github.com/OpenEugene/openboard/back/internal/pb" 13 | "github.com/codemodus/chain/v2" 14 | "github.com/codemodus/hedrs" 15 | "github.com/codemodus/mixmux" 16 | "github.com/codemodus/swagui" 17 | "github.com/codemodus/swagui/suidata3" 18 | "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 19 | "google.golang.org/grpc" 20 | ) 21 | 22 | // HTTPSrv ... 23 | type HTTPSrv struct { 24 | *http.Server 25 | gmux *runtime.ServeMux 26 | } 27 | 28 | // New ... 29 | func New(origins []string) (*HTTPSrv, error) { 30 | gmux := runtime.NewServeMux() // TODO: set options 31 | mux, err := multiplexer(time.Now(), gmux, origins) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | s := HTTPSrv{ 37 | Server: &http.Server{ 38 | Handler: mux, 39 | }, 40 | gmux: gmux, 41 | } 42 | 43 | return &s, nil 44 | } 45 | 46 | func multiplexer(start time.Time, gmux *runtime.ServeMux, origins []string) (http.Handler, error) { 47 | handleSwagger, err := swaggerHandler(start, "/v", "apidocs.swagger.json") 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | m := mixmux.NewTreeMux(nil) 53 | 54 | gm := http.StripPrefix("/v", gmux) 55 | m.Any("/v/", gm) 56 | m.Any("/v/*x", gm) 57 | 58 | m.Any("/v/docs/swagger.json", handleSwagger) 59 | 60 | if ui, err := swagui.New(http.NotFoundHandler(), suidata3.New()); err == nil { 61 | sh := http.StripPrefix("/v/docs", ui.Handler("/v/docs/swagger.json")) 62 | m.Get("/v/docs/", sh) 63 | m.Get("/v/docs/*x", sh) 64 | } 65 | 66 | origins = append(hedrs.DefaultOrigins, origins...) 67 | corsOrigins := hedrs.CORSOrigins(hedrs.NewAllowed(origins...)) 68 | corsMethods := hedrs.CORSMethods(hedrs.NewValues(hedrs.AllMethods...)) 69 | corsHeaders := hedrs.CORSHeaders(hedrs.NewValues(hedrs.DefaultHeaders...)) 70 | 71 | cmn := chain.New( 72 | corsOrigins, 73 | corsMethods, 74 | corsHeaders, 75 | ) 76 | 77 | return cmn.End(m), nil 78 | } 79 | 80 | // Serve ... 81 | func (s *HTTPSrv) Serve(rpcPort, httpPort string) error { 82 | opts := []grpc.DialOption{grpc.WithInsecure()} 83 | 84 | conn, err := grpc.Dial(rpcPort, opts...) 85 | if err != nil { 86 | return err 87 | } 88 | defer conn.Close() 89 | 90 | ctx, cancel := context.WithCancel(context.Background()) 91 | defer cancel() 92 | 93 | err = pb.RegisterAuthHandler(ctx, s.gmux, conn) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | err = pb.RegisterPostHandler(ctx, s.gmux, conn) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | err = pb.RegisterUserSvcHandler(ctx, s.gmux, conn) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | s.Server.Addr = httpPort 109 | 110 | if err = s.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 111 | return err 112 | } 113 | 114 | return nil 115 | } 116 | 117 | // Stop ... 118 | func (s *HTTPSrv) Stop() error { 119 | // TODO: setup shutdown context 120 | return s.Server.Shutdown(context.Background()) 121 | } 122 | 123 | func swaggerHandler(start time.Time, basePath, name string) (http.Handler, error) { 124 | fs, err := asset.NewFS() 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | f, err := fs.Open(name) 130 | if err != nil { 131 | return nil, err 132 | } 133 | defer f.Close() 134 | 135 | d, err := ioutil.ReadAll(f) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | if basePath != "" { 141 | if d, err = setSwaggerBasePath(basePath, d); err != nil { 142 | return nil, err 143 | } 144 | } 145 | 146 | mt := start 147 | info, err := f.Stat() 148 | if err == nil { 149 | mt = info.ModTime() 150 | } 151 | 152 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 153 | b := bytes.NewReader(d) 154 | http.ServeContent(w, r, name, mt, b) 155 | }) 156 | 157 | return h, nil 158 | } 159 | 160 | func setSwaggerBasePath(path string, d []byte) ([]byte, error) { 161 | var j swaggerJSON 162 | if err := json.Unmarshal(d, &j); err != nil { 163 | return nil, err 164 | } 165 | 166 | j.BasePath = path 167 | 168 | return json.Marshal(&j) 169 | } 170 | 171 | type swaggerJSON struct { 172 | Swagger string `json:"swagger"` 173 | /*Info struct { 174 | Title string `json:"title,omitempty"` 175 | Description string `json:"description,omitempty"` 176 | Version string `json:"version,omitempty"` 177 | }*/ 178 | Info json.RawMessage `json:"info,omitempty"` 179 | BasePath string `json:"basePath,omitempty"` 180 | Schemes json.RawMessage `json:"schemes,omitempty"` // []string 181 | Consumes json.RawMessage `json:"consumes,omitempty"` // []string 182 | Produces json.RawMessage `json:"produces,omitempty"` // []string 183 | Paths json.RawMessage `json:"paths,omitempty"` 184 | Definitions json.RawMessage `json:"definitions,omitempty"` 185 | } 186 | -------------------------------------------------------------------------------- /front/src/Ui.elm: -------------------------------------------------------------------------------- 1 | module Ui exposing (PostingKind(..), btn, card, flexBox, globalStyle, gridUnit, h1Style, heading, kindButton, kindToColor, linkBtn, mainContent, navBar, navBarList, pStyle, paragraph, postingBlurb, postingsList, theme) 2 | 3 | import Css exposing (..) 4 | import Css.Global exposing (global, selector) 5 | import Html.Styled exposing (..) 6 | import Html.Styled.Attributes exposing (..) 7 | import Route 8 | 9 | 10 | globalStyle = 11 | global 12 | [ selector "body" 13 | [ margin (px 0) 14 | ] 15 | ] 16 | 17 | 18 | 19 | -- Reusabled parts of the Ui used by Pages 20 | 21 | 22 | paragraph = 23 | styled p [ pStyle ] 24 | 25 | 26 | btn = 27 | styled button [ Css.batch [] ] 28 | 29 | 30 | {-| A reusable card module 31 | -} 32 | card : List (Attribute msg) -> List (Html msg) -> Html msg 33 | card = 34 | styled div 35 | [ borderRadius (px 4) 36 | , padding (px (gridUnit * 1)) 37 | , boxShadow4 (px 1) (px 1) (px 5) theme.dark 38 | , margin (px (8 * gridUnit)) 39 | ] 40 | 41 | 42 | {-| navBar 43 | -} 44 | navBar : List (Attribute msg) -> List (Html msg) -> Html msg 45 | navBar = 46 | styled nav 47 | [ boxShadow4 (px 1) (px 1) (px 5) theme.dark 48 | , displayFlex 49 | ] 50 | 51 | 52 | navBarList : List (Attribute msg) -> List (Html msg) -> Html msg 53 | navBarList = 54 | styled ul 55 | [ listStyleType none 56 | , margin (px 0) 57 | , padding (px 0) 58 | ] 59 | 60 | 61 | mainContent : List (Attribute msg) -> List (Html msg) -> Html msg 62 | mainContent = 63 | styled div 64 | [ maxWidth (px 960) 65 | , marginLeft auto 66 | , marginRight auto 67 | , padding (px 16) 68 | ] 69 | 70 | 71 | type PostingKind 72 | = Request 73 | | Offer 74 | 75 | 76 | kindToColor k = 77 | case k of 78 | Request -> 79 | theme.request 80 | 81 | Offer -> 82 | theme.offer 83 | 84 | 85 | flexBox = 86 | styled div 87 | [ displayFlex 88 | , marginBottom (px 10) 89 | , firstChild [ marginRight (px 10) ] 90 | ] 91 | 92 | 93 | kindButton : PostingKind -> List (Attribute msg) -> List (Html msg) -> Html msg 94 | kindButton k = 95 | styled button 96 | [ padding4 (px 8) (px 16) (px 8) (px 16) 97 | , color (rgb 250 250 250) 98 | , hover 99 | [ backgroundColor theme.dark 100 | , textDecoration underline 101 | ] 102 | , display block 103 | , backgroundColor (kindToColor k) 104 | , cursor pointer 105 | , border (px 0) 106 | , borderRadius (px 3) 107 | , fontSize (Css.em 2) 108 | ] 109 | 110 | 111 | postingsList = 112 | styled ul 113 | [ listStyleType none 114 | , margin (px 0) 115 | , padding (px 0) 116 | ] 117 | 118 | 119 | postingBlurb { title, body, slug } = 120 | styled article 121 | [ borderRadius (px 4) 122 | , padding (px (gridUnit * 1)) 123 | , boxShadow4 (px 1) (px 1) (px 5) theme.dark 124 | , marginBottom (px gridUnit) 125 | ] 126 | [] 127 | [ a [ Route.PostDetail slug |> Route.href ] [ heading title ], paragraph [] [ text body ] ] 128 | 129 | 130 | {-| A reusable heading 131 | -} 132 | heading : String -> Html msg 133 | heading title = 134 | styled h1 [ h1Style ] [] [ text title ] 135 | 136 | 137 | {-| A reusable link button 138 | -} 139 | linkBtn : List (Attribute msg) -> List (Html msg) -> Html msg 140 | linkBtn = 141 | styled a 142 | [ padding4 (px 8) (px 16) (px 8) (px 16) 143 | , color (rgb 250 250 250) 144 | , hover 145 | [ backgroundColor theme.dark 146 | , textDecoration underline 147 | ] 148 | , display block 149 | , margin (px 10) 150 | , backgroundColor theme.primary 151 | , cursor pointer 152 | , border (px 0) 153 | , borderRadius (px 3) 154 | , fontSize (Css.em 1) 155 | ] 156 | 157 | 158 | {-| Styles for h1 159 | -} 160 | h1Style : Style 161 | h1Style = 162 | Css.batch 163 | [ fontFamilies [ "Palatino Linotype", "Georgia", "serif" ] 164 | , fontSize (3 * gridUnit |> px) 165 | , fontWeight bold 166 | , marginTop (px 0) 167 | ] 168 | 169 | 170 | {-| Styles for h1 171 | -} 172 | pStyle : Style 173 | pStyle = 174 | Css.batch 175 | [ fontFamilies [ "Helvetica", "Arial", "sans-serif", "serif" ] 176 | , fontSize (2 * gridUnit |> px) 177 | , fontWeight normal 178 | , lineHeight (2 * gridUnit + gridUnit |> px) 179 | , color theme.dark 180 | , margin (px 0) 181 | ] 182 | 183 | 184 | {-| Theme colors 185 | -} 186 | theme = 187 | { primary = hex "000" 188 | , secondary = rgb 250 240 230 189 | , hover = hex "3ebc5b" 190 | , dark = hex "999" 191 | , request = hex "2135d1" 192 | , offer = hex "50b320" 193 | } 194 | 195 | 196 | {-| Grid unit to keep everything lined up and balanced 197 | -} 198 | gridUnit : Float 199 | gridUnit = 200 | 8 201 | -------------------------------------------------------------------------------- /front/bin/pb2elm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #? {cmd}: 4 | #? DEST -I INCLUDES... PROTOS... 5 | #? example: {cmd} src -I ../../msgs ../../msgs/proto/*.proto 6 | #? 7 | #? DEST is the root of the output source 8 | #? INCLUDES are the root directories from which to gather proto files 9 | #? PROTOS are generated in paths relative to the provided includes 10 | #? 11 | #? command: {cmd} src -I ../../msgs ../../msgs/proto/example.proto 12 | #? output: "Proto/Example.elm" within the "src" directory 13 | function curcmd() { 14 | echo "$(basename "${0}")" 15 | } 16 | 17 | function echerr() { 18 | echo "$(curcmd):" "${@}" >&2 && exit 1 19 | } 20 | 21 | function checkDep() { 22 | ! hash "${1}" 2>/dev/null && echerr "requires ${1}" 23 | } 24 | 25 | function checkDepOr() { 26 | ! (hash "${1}" 2>/dev/null || hash "${2}" 2>/dev/null) && echerr "requires ${1} or ${2}" 27 | } 28 | 29 | checkDep "protoc" 30 | checkDep "protoc-gen-elm" 31 | checkDep "elm-format" 32 | checkDepOr "sed" "gsed" 33 | checkDepOr "realpath" "grealpath" 34 | 35 | function depOr() { 36 | local ret="${1}" 37 | hash "${2}" 2>/dev/null && ret="${2}" 38 | echo "${ret}" 39 | } 40 | 41 | function xsed() { 42 | local cmd="$(depOr "sed" "gsed")" 43 | ${cmd} "${@}" 44 | } 45 | 46 | function xrealpath() { 47 | local cmd="$(depOr "realpath" "grealpath")" 48 | ${cmd} "${@}" 49 | } 50 | 51 | function yudodis_osx() { 52 | local sedCmd="$(depOr "sed" "gsed")" 53 | [[ "$(uname -s)" == "Darwin" && "${sedCmd}" == "sed" ]] && echerr "darwin requires gsed" 54 | } 55 | 56 | function getDestinationDir() { 57 | ! mkdir -p "${1}" 2>/dev/null && echerr "cannot make directory '${1}'" 58 | echo "${1}" 59 | } 60 | 61 | function getIncludeDirs() { 62 | local args=("${@}") 63 | local flag="-I" 64 | local rets=() 65 | 66 | for (( i=0; i<${#args}; i++ )); do 67 | local arg="${args[i]}" 68 | local argv="${args[i+1]}" 69 | 70 | [[ "${arg}" == "${flag}" ]] && rets+=("${arg}" "${argv}") && ((i++)); continue 71 | [[ "${arg}" == "${flag}"* ]] && rets+=("${arg}") && continue 72 | done 73 | 74 | echo "${rets[@]}" 75 | } 76 | 77 | function getSourceFiles() { 78 | local args=("${@}") 79 | local rets=() 80 | 81 | for arg in ${args[@]}; do 82 | [[ "${arg}" == "-"* ]] && continue 83 | 84 | [[ ! -f "${arg}" ]] && echerr "non-file '${arg}'" 85 | 86 | rets+=("${arg}") 87 | done 88 | 89 | [[ ${#rets[@]} == 0 ]] && echerr "must provide one or more files" 90 | echo "${rets[@]}" 91 | } 92 | 93 | function filterProto { 94 | local in="${1}" 95 | local out="${2}" 96 | 97 | cat "${in}" | \ 98 | # delete consecutive empty lines (simplifies multi-line empty message deletion) 99 | xsed '/^$/N;/^\n$/D' | \ 100 | # delete service definitions 101 | xsed '/^service/,/^}$/d' | \ 102 | # delete annotations.proto (contains proto2 dependency) 103 | xsed '/annotations.proto/d' | \ 104 | # delete empty messages (single line) 105 | xsed '/^message.*{\s*}/d' | \ 106 | # delete empty messages (multiple lines) 107 | xsed '/^message.*/ {N;N; /^message.*{[\s|\n]*\}/d}' | \ 108 | # delete consecutive empty lines (again, some have likely been created) 109 | xsed '/^$/N;/^\n$/D' | \ 110 | # delete last line if empty 111 | xsed '${/^$/d}' \ 112 | >> "${out}" 113 | 114 | return $? 115 | } 116 | 117 | function elmfmtMessage() { 118 | local elmfmt="$(elm-format -h 2>&1 | grep "^elm-format.*")" 119 | echo "-- ALSO PROCESSED BY ${elmfmt}" 120 | } 121 | 122 | function main { 123 | local cleanups=() 124 | function runCleanups() { 125 | for cleanup in "${cleanups[@]}"; do eval ${cleanup}; done 126 | } 127 | trap "runCleanups" EXIT RETURN 128 | 129 | local skip=1 130 | local dstDir incs files 131 | dstDir="$(getDestinationDir "${1}")" || exit 1 132 | ((skip+=1)) 133 | incs=($(getIncludeDirs ${@:${skip}})) || exit 1 134 | ((skip+=${#incs[@]})) 135 | files=($(getSourceFiles ${@:$skip})) || exit 1 136 | 137 | local token="xxfiltxx" 138 | local filtfiles=() 139 | 140 | for file in ${files[@]}; do 141 | local filtfile="${file/\.proto/${token}.proto}" 142 | ! filterProto "${file}" "${filtfile}" && continue 143 | filtfiles+=("${filtfile}") 144 | cleanups+=("rm ${filtfile}") 145 | done 146 | 147 | protoc ${incs[@]} --elm_out="${dstDir}" ${filtfiles[@]} > /dev/null 2>&1 148 | 149 | find "${dstDir}" -name "*${token}.elm" | while read file; do 150 | elm-format --yes "${file}" > /dev/null 151 | xsed -i 's/'${token}'//g' ${file} 152 | xsed -i '/^-- source.*/a '"$(elmfmtMessage)"'' ${file} 153 | local newfile=${file/${token}/} 154 | [[ "${file}" != "${newfile}" ]] && mv "${file}" "${newfile}" 155 | done 156 | } 157 | 158 | function doc() { 159 | function display() { 160 | grep '^#?' $(xrealpath "${0}") | \ 161 | xsed -e 's/#?\s*//g' -e 's/{cmd}/'"$(curcmd)"'/g' 162 | } 163 | 164 | local args=("${@}") 165 | for arg in ${args[@]}; do 166 | [[ "${arg}" =~ ^(help|-h|-help|--help)$ ]] && display && exit 0 167 | done 168 | } 169 | 170 | doc "${@}" 171 | yudodis_osx 172 | main "${@}" 173 | -------------------------------------------------------------------------------- /front/src/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (Model, Msg(..), init, main, update, view) 2 | 3 | import Browser exposing (Document, UrlRequest(..)) 4 | import Browser.Navigation as Nav 5 | import Html 6 | import Html.Styled 7 | import Json.Decode exposing (Value) 8 | import Page 9 | import Page.Home 10 | import Page.Login 11 | import Page.Posts 12 | import Route exposing (Route(..)) 13 | import Session 14 | import Url exposing (Url) 15 | 16 | 17 | 18 | ---- MODEL ---- 19 | 20 | 21 | type Model 22 | = Login Page.Login.Model 23 | | Home Page.Home.Model 24 | | Posts Page.Posts.Model 25 | 26 | 27 | 28 | --TODO: decode incoming JWT from localStorage and initialize Session accordingly 29 | 30 | 31 | init : () -> Url -> Nav.Key -> ( Model, Cmd Msg ) 32 | init _ url navKey = 33 | changeRouteTo (Route.fromUrl url) (Home (Page.Home.Model (Session.Guest navKey) "")) 34 | 35 | 36 | 37 | ---- UPDATE ---- 38 | 39 | 40 | type Msg 41 | = ChangedUrl Url 42 | | ClickedLink UrlRequest 43 | | GotHomeMsg Page.Home.Msg 44 | | GotLoginMsg Page.Login.Msg 45 | | GotPostsMsg Page.Posts.Msg 46 | | GotJwt Value 47 | 48 | 49 | update : Msg -> Model -> ( Model, Cmd Msg ) 50 | update msg model = 51 | case ( msg, model ) of 52 | ( ChangedUrl url, _ ) -> 53 | changeRouteTo (Route.fromUrl url) model 54 | 55 | ( ClickedLink urlRequest, _ ) -> 56 | case urlRequest of 57 | Internal url -> 58 | ( model, Nav.pushUrl (Session.navKey (toSession model)) (Url.toString url) ) 59 | 60 | External url -> 61 | ( model, Nav.load url ) 62 | 63 | ( GotHomeMsg subMsg, Home home ) -> 64 | Page.Home.update subMsg home 65 | |> updateWith Home GotHomeMsg model 66 | 67 | ( GotLoginMsg subMsg, Login home ) -> 68 | Page.Login.update subMsg home 69 | |> updateWith Login GotLoginMsg model 70 | 71 | ( GotPostsMsg subMsg, Posts home ) -> 72 | Page.Posts.update subMsg home 73 | |> updateWith Posts GotPostsMsg model 74 | 75 | ( _, _ ) -> 76 | -- Disregard messages that arrived for the wrong page. 77 | ( model, Cmd.none ) 78 | 79 | 80 | updateWith : (subModel -> Model) -> (subMsg -> Msg) -> Model -> ( subModel, Cmd subMsg ) -> ( Model, Cmd Msg ) 81 | updateWith toModel toMsg _ ( subModel, subCmd ) = 82 | ( toModel subModel 83 | , Cmd.map toMsg subCmd 84 | ) 85 | 86 | 87 | changeRouteTo : Maybe Route -> Model -> ( Model, Cmd Msg ) 88 | changeRouteTo maybeRoute model = 89 | case maybeRoute of 90 | Just Route.Home -> 91 | Page.Home.init (toSession model) 92 | |> updateWith Home GotHomeMsg model 93 | 94 | Just Route.Login -> 95 | Page.Login.init (toSession model) 96 | |> updateWith Login GotLoginMsg model 97 | 98 | Just Route.NewRequest -> 99 | Page.Posts.init (toSession model) Page.Posts.Request "" 100 | |> updateWith Posts GotPostsMsg model 101 | 102 | Just Route.NewOffer -> 103 | Page.Posts.init (toSession model) Page.Posts.Offer "" 104 | |> updateWith Posts GotPostsMsg model 105 | 106 | Just (Route.PostDetail slug) -> 107 | Page.Posts.init (toSession model) Page.Posts.Unknown slug 108 | |> updateWith Posts GotPostsMsg model 109 | 110 | Just (Route.EditPost slug) -> 111 | Page.Posts.init (toSession model) Page.Posts.Unknown slug 112 | |> updateWith Posts GotPostsMsg model 113 | 114 | Nothing -> 115 | ( model, Cmd.none ) 116 | 117 | 118 | toSession : Model -> Session.Session 119 | toSession model = 120 | case model of 121 | Login loginModel -> 122 | Page.Login.toSession loginModel 123 | 124 | Home homeModel -> 125 | Page.Home.toSession homeModel 126 | 127 | Posts postsModel -> 128 | Page.Posts.toSession postsModel 129 | 130 | 131 | 132 | ---- VIEW ---- 133 | 134 | 135 | view : Model -> Document Msg 136 | view model = 137 | let 138 | viewPage : Page.Page -> (msg -> Msg) -> { title : String, content : Html.Styled.Html msg } -> Document Msg 139 | viewPage page toMsg config = 140 | let 141 | { title, body } = 142 | Page.view (Session.viewer (toSession model)) page config 143 | in 144 | { title = title 145 | , body = List.map (Html.map toMsg) body 146 | } 147 | in 148 | case model of 149 | Home home -> 150 | viewPage Page.Home GotHomeMsg (Page.Home.view home) 151 | 152 | Login login -> 153 | viewPage Page.Other GotLoginMsg (Page.Login.view login) 154 | 155 | Posts posts -> 156 | viewPage Page.Other GotPostsMsg (Page.Posts.view posts) 157 | 158 | 159 | 160 | -- Ports 161 | -- port storeCache : Maybe Value -> Cmd msg 162 | 163 | 164 | port onStoreChange : (Value -> msg) -> Sub msg 165 | 166 | 167 | subscriptions : Model -> Sub Msg 168 | subscriptions model = 169 | onStoreChange GotJwt 170 | 171 | 172 | 173 | ---- PROGRAM ---- 174 | 175 | 176 | main : Program () Model Msg 177 | main = 178 | Browser.application 179 | { init = init 180 | , onUrlChange = ChangedUrl 181 | , onUrlRequest = ClickedLink 182 | , subscriptions = subscriptions 183 | , update = update 184 | , view = view 185 | } 186 | -------------------------------------------------------------------------------- /back/internal/pb/auth_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package pb 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // AuthClient is the client API for Auth service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type AuthClient interface { 21 | AddAuth(ctx context.Context, in *AddAuthReq, opts ...grpc.CallOption) (*AuthResp, error) 22 | RmvAuth(ctx context.Context, in *RmvAuthReq, opts ...grpc.CallOption) (*RmvAuthResp, error) 23 | AddVoucher(ctx context.Context, in *AddVoucherReq, opts ...grpc.CallOption) (*AddVoucherResp, error) 24 | } 25 | 26 | type authClient struct { 27 | cc grpc.ClientConnInterface 28 | } 29 | 30 | func NewAuthClient(cc grpc.ClientConnInterface) AuthClient { 31 | return &authClient{cc} 32 | } 33 | 34 | func (c *authClient) AddAuth(ctx context.Context, in *AddAuthReq, opts ...grpc.CallOption) (*AuthResp, error) { 35 | out := new(AuthResp) 36 | err := c.cc.Invoke(ctx, "/pb.Auth/AddAuth", in, out, opts...) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return out, nil 41 | } 42 | 43 | func (c *authClient) RmvAuth(ctx context.Context, in *RmvAuthReq, opts ...grpc.CallOption) (*RmvAuthResp, error) { 44 | out := new(RmvAuthResp) 45 | err := c.cc.Invoke(ctx, "/pb.Auth/RmvAuth", in, out, opts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return out, nil 50 | } 51 | 52 | func (c *authClient) AddVoucher(ctx context.Context, in *AddVoucherReq, opts ...grpc.CallOption) (*AddVoucherResp, error) { 53 | out := new(AddVoucherResp) 54 | err := c.cc.Invoke(ctx, "/pb.Auth/AddVoucher", in, out, opts...) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return out, nil 59 | } 60 | 61 | // AuthServer is the server API for Auth service. 62 | // All implementations should embed UnimplementedAuthServer 63 | // for forward compatibility 64 | type AuthServer interface { 65 | AddAuth(context.Context, *AddAuthReq) (*AuthResp, error) 66 | RmvAuth(context.Context, *RmvAuthReq) (*RmvAuthResp, error) 67 | AddVoucher(context.Context, *AddVoucherReq) (*AddVoucherResp, error) 68 | } 69 | 70 | // UnimplementedAuthServer should be embedded to have forward compatible implementations. 71 | type UnimplementedAuthServer struct { 72 | } 73 | 74 | func (UnimplementedAuthServer) AddAuth(context.Context, *AddAuthReq) (*AuthResp, error) { 75 | return nil, status.Errorf(codes.Unimplemented, "method AddAuth not implemented") 76 | } 77 | func (UnimplementedAuthServer) RmvAuth(context.Context, *RmvAuthReq) (*RmvAuthResp, error) { 78 | return nil, status.Errorf(codes.Unimplemented, "method RmvAuth not implemented") 79 | } 80 | func (UnimplementedAuthServer) AddVoucher(context.Context, *AddVoucherReq) (*AddVoucherResp, error) { 81 | return nil, status.Errorf(codes.Unimplemented, "method AddVoucher not implemented") 82 | } 83 | 84 | // UnsafeAuthServer may be embedded to opt out of forward compatibility for this service. 85 | // Use of this interface is not recommended, as added methods to AuthServer will 86 | // result in compilation errors. 87 | type UnsafeAuthServer interface { 88 | mustEmbedUnimplementedAuthServer() 89 | } 90 | 91 | func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) { 92 | s.RegisterService(&Auth_ServiceDesc, srv) 93 | } 94 | 95 | func _Auth_AddAuth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 96 | in := new(AddAuthReq) 97 | if err := dec(in); err != nil { 98 | return nil, err 99 | } 100 | if interceptor == nil { 101 | return srv.(AuthServer).AddAuth(ctx, in) 102 | } 103 | info := &grpc.UnaryServerInfo{ 104 | Server: srv, 105 | FullMethod: "/pb.Auth/AddAuth", 106 | } 107 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 108 | return srv.(AuthServer).AddAuth(ctx, req.(*AddAuthReq)) 109 | } 110 | return interceptor(ctx, in, info, handler) 111 | } 112 | 113 | func _Auth_RmvAuth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 114 | in := new(RmvAuthReq) 115 | if err := dec(in); err != nil { 116 | return nil, err 117 | } 118 | if interceptor == nil { 119 | return srv.(AuthServer).RmvAuth(ctx, in) 120 | } 121 | info := &grpc.UnaryServerInfo{ 122 | Server: srv, 123 | FullMethod: "/pb.Auth/RmvAuth", 124 | } 125 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 126 | return srv.(AuthServer).RmvAuth(ctx, req.(*RmvAuthReq)) 127 | } 128 | return interceptor(ctx, in, info, handler) 129 | } 130 | 131 | func _Auth_AddVoucher_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 132 | in := new(AddVoucherReq) 133 | if err := dec(in); err != nil { 134 | return nil, err 135 | } 136 | if interceptor == nil { 137 | return srv.(AuthServer).AddVoucher(ctx, in) 138 | } 139 | info := &grpc.UnaryServerInfo{ 140 | Server: srv, 141 | FullMethod: "/pb.Auth/AddVoucher", 142 | } 143 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 144 | return srv.(AuthServer).AddVoucher(ctx, req.(*AddVoucherReq)) 145 | } 146 | return interceptor(ctx, in, info, handler) 147 | } 148 | 149 | // Auth_ServiceDesc is the grpc.ServiceDesc for Auth service. 150 | // It's only intended for direct use with grpc.RegisterService, 151 | // and not to be introspected or modified (even as a copy) 152 | var Auth_ServiceDesc = grpc.ServiceDesc{ 153 | ServiceName: "pb.Auth", 154 | HandlerType: (*AuthServer)(nil), 155 | Methods: []grpc.MethodDesc{ 156 | { 157 | MethodName: "AddAuth", 158 | Handler: _Auth_AddAuth_Handler, 159 | }, 160 | { 161 | MethodName: "RmvAuth", 162 | Handler: _Auth_RmvAuth_Handler, 163 | }, 164 | { 165 | MethodName: "AddVoucher", 166 | Handler: _Auth_AddVoucher_Handler, 167 | }, 168 | }, 169 | Streams: []grpc.StreamDesc{}, 170 | Metadata: "auth.proto", 171 | } 172 | -------------------------------------------------------------------------------- /msgs/proto/include/googleapis/google/rpc/code.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.rpc; 18 | 19 | option go_package = "google.golang.org/genproto/googleapis/rpc/code;code"; 20 | option java_multiple_files = true; 21 | option java_outer_classname = "CodeProto"; 22 | option java_package = "com.google.rpc"; 23 | option objc_class_prefix = "RPC"; 24 | 25 | 26 | // The canonical error codes for Google APIs. 27 | // 28 | // 29 | // Sometimes multiple error codes may apply. Services should return 30 | // the most specific error code that applies. For example, prefer 31 | // `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. 32 | // Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. 33 | enum Code { 34 | // Not an error; returned on success 35 | // 36 | // HTTP Mapping: 200 OK 37 | OK = 0; 38 | 39 | // The operation was cancelled, typically by the caller. 40 | // 41 | // HTTP Mapping: 499 Client Closed Request 42 | CANCELLED = 1; 43 | 44 | // Unknown error. For example, this error may be returned when 45 | // a `Status` value received from another address space belongs to 46 | // an error space that is not known in this address space. Also 47 | // errors raised by APIs that do not return enough error information 48 | // may be converted to this error. 49 | // 50 | // HTTP Mapping: 500 Internal Server Error 51 | UNKNOWN = 2; 52 | 53 | // The client specified an invalid argument. Note that this differs 54 | // from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments 55 | // that are problematic regardless of the state of the system 56 | // (e.g., a malformed file name). 57 | // 58 | // HTTP Mapping: 400 Bad Request 59 | INVALID_ARGUMENT = 3; 60 | 61 | // The deadline expired before the operation could complete. For operations 62 | // that change the state of the system, this error may be returned 63 | // even if the operation has completed successfully. For example, a 64 | // successful response from a server could have been delayed long 65 | // enough for the deadline to expire. 66 | // 67 | // HTTP Mapping: 504 Gateway Timeout 68 | DEADLINE_EXCEEDED = 4; 69 | 70 | // Some requested entity (e.g., file or directory) was not found. 71 | // 72 | // Note to server developers: if a request is denied for an entire class 73 | // of users, such as gradual feature rollout or undocumented whitelist, 74 | // `NOT_FOUND` may be used. If a request is denied for some users within 75 | // a class of users, such as user-based access control, `PERMISSION_DENIED` 76 | // must be used. 77 | // 78 | // HTTP Mapping: 404 Not Found 79 | NOT_FOUND = 5; 80 | 81 | // The entity that a client attempted to create (e.g., file or directory) 82 | // already exists. 83 | // 84 | // HTTP Mapping: 409 Conflict 85 | ALREADY_EXISTS = 6; 86 | 87 | // The caller does not have permission to execute the specified 88 | // operation. `PERMISSION_DENIED` must not be used for rejections 89 | // caused by exhausting some resource (use `RESOURCE_EXHAUSTED` 90 | // instead for those errors). `PERMISSION_DENIED` must not be 91 | // used if the caller can not be identified (use `UNAUTHENTICATED` 92 | // instead for those errors). This error code does not imply the 93 | // request is valid or the requested entity exists or satisfies 94 | // other pre-conditions. 95 | // 96 | // HTTP Mapping: 403 Forbidden 97 | PERMISSION_DENIED = 7; 98 | 99 | // The request does not have valid authentication credentials for the 100 | // operation. 101 | // 102 | // HTTP Mapping: 401 Unauthorized 103 | UNAUTHENTICATED = 16; 104 | 105 | // Some resource has been exhausted, perhaps a per-user quota, or 106 | // perhaps the entire file system is out of space. 107 | // 108 | // HTTP Mapping: 429 Too Many Requests 109 | RESOURCE_EXHAUSTED = 8; 110 | 111 | // The operation was rejected because the system is not in a state 112 | // required for the operation's execution. For example, the directory 113 | // to be deleted is non-empty, an rmdir operation is applied to 114 | // a non-directory, etc. 115 | // 116 | // Service implementors can use the following guidelines to decide 117 | // between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: 118 | // (a) Use `UNAVAILABLE` if the client can retry just the failing call. 119 | // (b) Use `ABORTED` if the client should retry at a higher level 120 | // (e.g., when a client-specified test-and-set fails, indicating the 121 | // client should restart a read-modify-write sequence). 122 | // (c) Use `FAILED_PRECONDITION` if the client should not retry until 123 | // the system state has been explicitly fixed. E.g., if an "rmdir" 124 | // fails because the directory is non-empty, `FAILED_PRECONDITION` 125 | // should be returned since the client should not retry unless 126 | // the files are deleted from the directory. 127 | // 128 | // HTTP Mapping: 400 Bad Request 129 | FAILED_PRECONDITION = 9; 130 | 131 | // The operation was aborted, typically due to a concurrency issue such as 132 | // a sequencer check failure or transaction abort. 133 | // 134 | // See the guidelines above for deciding between `FAILED_PRECONDITION`, 135 | // `ABORTED`, and `UNAVAILABLE`. 136 | // 137 | // HTTP Mapping: 409 Conflict 138 | ABORTED = 10; 139 | 140 | // The operation was attempted past the valid range. E.g., seeking or 141 | // reading past end-of-file. 142 | // 143 | // Unlike `INVALID_ARGUMENT`, this error indicates a problem that may 144 | // be fixed if the system state changes. For example, a 32-bit file 145 | // system will generate `INVALID_ARGUMENT` if asked to read at an 146 | // offset that is not in the range [0,2^32-1], but it will generate 147 | // `OUT_OF_RANGE` if asked to read from an offset past the current 148 | // file size. 149 | // 150 | // There is a fair bit of overlap between `FAILED_PRECONDITION` and 151 | // `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific 152 | // error) when it applies so that callers who are iterating through 153 | // a space can easily look for an `OUT_OF_RANGE` error to detect when 154 | // they are done. 155 | // 156 | // HTTP Mapping: 400 Bad Request 157 | OUT_OF_RANGE = 11; 158 | 159 | // The operation is not implemented or is not supported/enabled in this 160 | // service. 161 | // 162 | // HTTP Mapping: 501 Not Implemented 163 | UNIMPLEMENTED = 12; 164 | 165 | // Internal errors. This means that some invariants expected by the 166 | // underlying system have been broken. This error code is reserved 167 | // for serious errors. 168 | // 169 | // HTTP Mapping: 500 Internal Server Error 170 | INTERNAL = 13; 171 | 172 | // The service is currently unavailable. This is most likely a 173 | // transient condition, which can be corrected by retrying with 174 | // a backoff. 175 | // 176 | // See the guidelines above for deciding between `FAILED_PRECONDITION`, 177 | // `ABORTED`, and `UNAVAILABLE`. 178 | // 179 | // HTTP Mapping: 503 Service Unavailable 180 | UNAVAILABLE = 14; 181 | 182 | // Unrecoverable data loss or corruption. 183 | // 184 | // HTTP Mapping: 500 Internal Server Error 185 | DATA_LOSS = 15; 186 | } 187 | -------------------------------------------------------------------------------- /back/tests/openbsrv/usersvc_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/OpenEugene/openboard/back/internal/pb" 9 | "google.golang.org/grpc" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | func TestClientServices(t *testing.T) { 14 | ctx := context.Background() 15 | conn, err := grpc.Dial(":4242", grpc.WithInsecure()) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | defer conn.Close() 20 | 21 | // User Service Tests 22 | userClnt := pb.NewUserSvcClient(conn) 23 | t.Run("Add and find user role", userSvcAddAndFndRoleFn(ctx, conn, userClnt)) 24 | t.Run("Add and find users", userSvcAddAndFndUsersFn(ctx, conn, userClnt)) 25 | t.Run("Find and delete user", userSvcDelUserFn(ctx, conn, userClnt)) 26 | } 27 | 28 | func userSvcAddAndFndRoleFn(ctx context.Context, conn *grpc.ClientConn, clnt pb.UserSvcClient) func(*testing.T) { 29 | return func(t *testing.T) { 30 | tests := []struct { 31 | desc string 32 | want string 33 | addReq *pb.AddRoleReq 34 | fndReq *pb.FndRolesReq 35 | }{ 36 | { 37 | "add and find user role", 38 | "user", 39 | &pb.AddRoleReq{Name: "user"}, 40 | &pb.FndRolesReq{ 41 | RoleIds: []string{}, 42 | RoleNames: []string{"user"}, 43 | Limit: 100, 44 | Lapse: 0, 45 | }, 46 | }, 47 | { 48 | "add and find admin role", 49 | "admin", 50 | &pb.AddRoleReq{Name: "admin"}, 51 | &pb.FndRolesReq{ 52 | RoleIds: []string{}, 53 | RoleNames: []string{"admin"}, 54 | Limit: 100, 55 | Lapse: 0, 56 | }, 57 | }, 58 | } 59 | 60 | for _, tt := range tests { 61 | _, err := clnt.AddRole(ctx, tt.addReq) 62 | if err != nil { 63 | t.Fatalf("%s: adding role %v: %v", tt.desc, tt.want, err) 64 | } 65 | 66 | r, err := clnt.FndRoles(ctx, tt.fndReq) 67 | if err != nil { 68 | t.Fatalf("%s: finding role %v: %v", tt.desc, tt.want, err) 69 | } 70 | 71 | if len(r.Items) == 0 { 72 | t.Fatalf("%s: got: no items, want: %s", tt.desc, tt.want) 73 | } 74 | 75 | if len(r.Items) > 1 { 76 | t.Fatalf("%s: got: %+v, want: %s", tt.desc, r, tt.want) 77 | } 78 | 79 | if got := r.Items[0].Name; got != tt.want { 80 | t.Fatalf("%s: got: %v, want: %s", tt.desc, got, tt.want) 81 | } 82 | } 83 | } 84 | } 85 | 86 | func userSvcAddAndFndUsersFn(ctx context.Context, conn *grpc.ClientConn, clnt pb.UserSvcClient) func(*testing.T) { 87 | return func(t *testing.T) { 88 | fndAdminRoleIdReq := &pb.FndRolesReq{ 89 | RoleIds: []string{}, 90 | RoleNames: []string{"admin"}, 91 | Limit: 100, 92 | Lapse: 0, 93 | } 94 | 95 | fndUserRoleIdReq := &pb.FndRolesReq{ 96 | RoleIds: []string{}, 97 | RoleNames: []string{"user"}, 98 | Limit: 100, 99 | Lapse: 0, 100 | } 101 | 102 | adminRoleID, err := roleID(ctx, conn, clnt, fndAdminRoleIdReq) 103 | if err != nil { 104 | t.Fatalf("retrieving role ID from role name: %v", err) 105 | } 106 | 107 | userRoleID, err := roleID(ctx, conn, clnt, fndUserRoleIdReq) 108 | if err != nil { 109 | t.Fatalf("retrieving role ID from role name: %v", err) 110 | } 111 | 112 | tests := []struct { 113 | desc string 114 | addUserReq *pb.AddUserReq 115 | want *pb.User 116 | fndUserReq *pb.FndUsersReq 117 | }{ 118 | { 119 | "Add and find user A", 120 | &pb.AddUserReq{ 121 | Username: "test username A", 122 | Email: "user_a@email.com", 123 | EmailHold: false, 124 | Altmail: "", 125 | AltmailHold: false, 126 | FullName: "test user full name A", 127 | Avatar: "test user avatar A", 128 | Password: "test user password A", 129 | RoleIds: []string{userRoleID}, 130 | }, 131 | &pb.User{ 132 | Username: "test username A", 133 | Email: "user_a@email.com", 134 | EmailHold: false, 135 | Altmail: "", 136 | AltmailHold: false, 137 | FullName: "test user full name A", 138 | Avatar: "test user avatar A", 139 | Roles: []*pb.RoleResp{{Name: "user"}}, 140 | }, 141 | &pb.FndUsersReq{ 142 | RoleIds: []string{}, 143 | Email: "user_a@email.com", 144 | EmailHold: false, 145 | Altmail: "", 146 | AltmailHold: false, 147 | Limit: 100, 148 | Lapse: 0, 149 | }, 150 | }, 151 | { 152 | "Add and find user B", 153 | &pb.AddUserReq{ 154 | Username: "test username B", 155 | Email: "userB@email.com", 156 | EmailHold: false, 157 | Altmail: "", 158 | AltmailHold: false, 159 | FullName: "test user full name B", 160 | Avatar: "test user avatar B", 161 | Password: "test user password B", 162 | RoleIds: []string{userRoleID, adminRoleID}, 163 | }, 164 | &pb.User{ 165 | Username: "test username B", 166 | Email: "userB@email.com", 167 | EmailHold: false, 168 | Altmail: "", 169 | AltmailHold: false, 170 | FullName: "test user full name B", 171 | Avatar: "test user avatar B", 172 | Roles: []*pb.RoleResp{ 173 | {Name: "user"}, 174 | {Name: "admin"}, 175 | }, 176 | }, 177 | &pb.FndUsersReq{ 178 | RoleIds: []string{}, 179 | Email: "userB@email.com", 180 | EmailHold: false, 181 | Altmail: "", 182 | AltmailHold: false, 183 | Limit: 100, 184 | Lapse: 0, 185 | }, 186 | }, 187 | } 188 | 189 | for _, tc := range tests { 190 | r, err := clnt.AddUser(ctx, tc.addUserReq) 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | 195 | got := r.Item 196 | wantUserID := got.Id 197 | 198 | unsetUntestedFields(got) 199 | 200 | for i := 0; i < len(got.Roles); i++ { 201 | unsetUntestedFields(got.Roles[i]) 202 | } 203 | 204 | if !proto.Equal(got, tc.want) { 205 | t.Fatalf("%s: got: %#v, want: %#v", tc.desc, got, tc.want) 206 | } 207 | 208 | gotUserID, err := userSvcFndUser(ctx, conn, clnt, tc.fndUserReq) 209 | if err != nil { 210 | t.Errorf("unable to find user: %v", err) 211 | } 212 | if gotUserID != wantUserID { 213 | t.Fatalf("got: %s, want: %s", gotUserID, wantUserID) 214 | } 215 | } 216 | } 217 | } 218 | 219 | func userSvcDelUserFn(ctx context.Context, conn *grpc.ClientConn, clnt pb.UserSvcClient) func(*testing.T) { 220 | return func(t *testing.T) { 221 | req := &pb.FndUsersReq{ 222 | RoleIds: []string{}, 223 | Email: "user_a@email.com", 224 | EmailHold: false, 225 | Altmail: "", 226 | AltmailHold: false, 227 | Limit: 100, 228 | Lapse: 0, 229 | } 230 | 231 | userID, err := userSvcFndUser(ctx, conn, clnt, req) 232 | if err != nil { 233 | t.Error(err) 234 | } 235 | 236 | if userID == "" { 237 | t.Fatalf("unable to find user %s", userID) 238 | } 239 | 240 | _, err = clnt.RmvUser(ctx, &pb.RmvUserReq{Id: userID}) 241 | if err != nil { 242 | t.Error(err) 243 | } 244 | 245 | resp, err := clnt.FndUsers(ctx, req) 246 | if err != nil { 247 | t.Error(err) 248 | } 249 | 250 | if resp.Items[0].Deleted == nil { 251 | t.Fatalf("expected user %s deleted_at to not be nil", resp.Items[0].Id) 252 | t.Fail() 253 | } 254 | } 255 | } 256 | 257 | func roleID(ctx context.Context, conn *grpc.ClientConn, clnt pb.UserSvcClient, req *pb.FndRolesReq) (string, error) { 258 | r, err := clnt.FndRoles(ctx, req) 259 | if err != nil { 260 | return "", err 261 | } 262 | 263 | if len(r.Items) == 0 { 264 | return "", nil 265 | } 266 | 267 | return r.Items[0].Id, nil 268 | } 269 | 270 | func userSvcFndUser(ctx context.Context, conn *grpc.ClientConn, clnt pb.UserSvcClient, req *pb.FndUsersReq) (string, error) { 271 | r, err := clnt.FndUsers(ctx, req) 272 | if err != nil { 273 | return "", fmt.Errorf("unable to find user: %w", err) 274 | } 275 | if len(r.Items) > 0 { 276 | return r.Items[0].Id, nil 277 | } 278 | return "", nil 279 | } 280 | -------------------------------------------------------------------------------- /msgs/proto/include/googleapis/google/rpc/error_details.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.rpc; 18 | 19 | import "google/protobuf/duration.proto"; 20 | 21 | option go_package = "google.golang.org/genproto/googleapis/rpc/errdetails;errdetails"; 22 | option java_multiple_files = true; 23 | option java_outer_classname = "ErrorDetailsProto"; 24 | option java_package = "com.google.rpc"; 25 | option objc_class_prefix = "RPC"; 26 | 27 | 28 | // Describes when the clients can retry a failed request. Clients could ignore 29 | // the recommendation here or retry when this information is missing from error 30 | // responses. 31 | // 32 | // It's always recommended that clients should use exponential backoff when 33 | // retrying. 34 | // 35 | // Clients should wait until `retry_delay` amount of time has passed since 36 | // receiving the error response before retrying. If retrying requests also 37 | // fail, clients should use an exponential backoff scheme to gradually increase 38 | // the delay between retries based on `retry_delay`, until either a maximum 39 | // number of retires have been reached or a maximum retry delay cap has been 40 | // reached. 41 | message RetryInfo { 42 | // Clients should wait at least this long between retrying the same request. 43 | google.protobuf.Duration retry_delay = 1; 44 | } 45 | 46 | // Describes additional debugging info. 47 | message DebugInfo { 48 | // The stack trace entries indicating where the error occurred. 49 | repeated string stack_entries = 1; 50 | 51 | // Additional debugging information provided by the server. 52 | string detail = 2; 53 | } 54 | 55 | // Describes how a quota check failed. 56 | // 57 | // For example if a daily limit was exceeded for the calling project, 58 | // a service could respond with a QuotaFailure detail containing the project 59 | // id and the description of the quota limit that was exceeded. If the 60 | // calling project hasn't enabled the service in the developer console, then 61 | // a service could respond with the project id and set `service_disabled` 62 | // to true. 63 | // 64 | // Also see RetryDetail and Help types for other details about handling a 65 | // quota failure. 66 | message QuotaFailure { 67 | // A message type used to describe a single quota violation. For example, a 68 | // daily quota or a custom quota that was exceeded. 69 | message Violation { 70 | // The subject on which the quota check failed. 71 | // For example, "clientip:" or "project:". 73 | string subject = 1; 74 | 75 | // A description of how the quota check failed. Clients can use this 76 | // description to find more about the quota configuration in the service's 77 | // public documentation, or find the relevant quota limit to adjust through 78 | // developer console. 79 | // 80 | // For example: "Service disabled" or "Daily Limit for read operations 81 | // exceeded". 82 | string description = 2; 83 | } 84 | 85 | // Describes all quota violations. 86 | repeated Violation violations = 1; 87 | } 88 | 89 | // Describes what preconditions have failed. 90 | // 91 | // For example, if an RPC failed because it required the Terms of Service to be 92 | // acknowledged, it could list the terms of service violation in the 93 | // PreconditionFailure message. 94 | message PreconditionFailure { 95 | // A message type used to describe a single precondition failure. 96 | message Violation { 97 | // The type of PreconditionFailure. We recommend using a service-specific 98 | // enum type to define the supported precondition violation types. For 99 | // example, "TOS" for "Terms of Service violation". 100 | string type = 1; 101 | 102 | // The subject, relative to the type, that failed. 103 | // For example, "google.com/cloud" relative to the "TOS" type would 104 | // indicate which terms of service is being referenced. 105 | string subject = 2; 106 | 107 | // A description of how the precondition failed. Developers can use this 108 | // description to understand how to fix the failure. 109 | // 110 | // For example: "Terms of service not accepted". 111 | string description = 3; 112 | } 113 | 114 | // Describes all precondition violations. 115 | repeated Violation violations = 1; 116 | } 117 | 118 | // Describes violations in a client request. This error type focuses on the 119 | // syntactic aspects of the request. 120 | message BadRequest { 121 | // A message type used to describe a single bad request field. 122 | message FieldViolation { 123 | // A path leading to a field in the request body. The value will be a 124 | // sequence of dot-separated identifiers that identify a protocol buffer 125 | // field. E.g., "field_violations.field" would identify this field. 126 | string field = 1; 127 | 128 | // A description of why the request element is bad. 129 | string description = 2; 130 | } 131 | 132 | // Describes all violations in a client request. 133 | repeated FieldViolation field_violations = 1; 134 | } 135 | 136 | // Contains metadata about the request that clients can attach when filing a bug 137 | // or providing other forms of feedback. 138 | message RequestInfo { 139 | // An opaque string that should only be interpreted by the service generating 140 | // it. For example, it can be used to identify requests in the service's logs. 141 | string request_id = 1; 142 | 143 | // Any data that was used to serve this request. For example, an encrypted 144 | // stack trace that can be sent back to the service provider for debugging. 145 | string serving_data = 2; 146 | } 147 | 148 | // Describes the resource that is being accessed. 149 | message ResourceInfo { 150 | // A name for the type of resource being accessed, e.g. "sql table", 151 | // "cloud storage bucket", "file", "Google calendar"; or the type URL 152 | // of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic". 153 | string resource_type = 1; 154 | 155 | // The name of the resource being accessed. For example, a shared calendar 156 | // name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current 157 | // error is [google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED]. 158 | string resource_name = 2; 159 | 160 | // The owner of the resource (optional). 161 | // For example, "user:" or "project:". 163 | string owner = 3; 164 | 165 | // Describes what error is encountered when accessing this resource. 166 | // For example, updating a cloud project may require the `writer` permission 167 | // on the developer console project. 168 | string description = 4; 169 | } 170 | 171 | // Provides links to documentation or for performing an out of band action. 172 | // 173 | // For example, if a quota check failed with an error indicating the calling 174 | // project hasn't enabled the accessed service, this can contain a URL pointing 175 | // directly to the right place in the developer console to flip the bit. 176 | message Help { 177 | // Describes a URL link. 178 | message Link { 179 | // Describes what the link offers. 180 | string description = 1; 181 | 182 | // The URL of the link. 183 | string url = 2; 184 | } 185 | 186 | // URL(s) pointing to additional information on handling the current error. 187 | repeated Link links = 1; 188 | } 189 | 190 | // Provides a localized error message that is safe to return to the user 191 | // which can be attached to an RPC error. 192 | message LocalizedMessage { 193 | // The locale used following the specification defined at 194 | // http://www.rfc-editor.org/rfc/bcp/bcp47.txt. 195 | // Examples are: "en-US", "fr-CH", "es-MX" 196 | string locale = 1; 197 | 198 | // The localized error message in the above locale. 199 | string message = 2; 200 | } 201 | -------------------------------------------------------------------------------- /back/internal/usersvc/internal/userdb/qryuser.go: -------------------------------------------------------------------------------- 1 | package userdb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/OpenEugene/openboard/back/internal/altr" 10 | "github.com/OpenEugene/openboard/back/internal/pb" 11 | "github.com/codemodus/uidgen" 12 | "github.com/go-sql-driver/mysql" 13 | "github.com/jmoiron/sqlx" 14 | ) 15 | 16 | type cx = context.Context 17 | 18 | var ( 19 | csvStr = altr.CSVFromStrings 20 | lim = altr.LimitUint32 21 | asTS = altr.Timestamp 22 | ) 23 | 24 | func parseOrUID(ug *uidgen.UIDGen, sid string) (uidgen.UID, bool) { 25 | if sid == "" { 26 | return ug.UID(), true 27 | } 28 | return ug.Parse(sid) 29 | } 30 | 31 | func (s *UserDB) upsertUser(ctx cx, sid string, x *pb.AddUserReq, y *pb.UserResp) error { 32 | id, ok := parseOrUID(s.ug, sid) 33 | if !ok { 34 | return fmt.Errorf("invalid uid") 35 | } 36 | 37 | tx, err := s.db.BeginTx(ctx, nil) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | qry := ` 43 | INSERT INTO user ( 44 | user_id, username, email, email_hold, altmail, altmail_hold, full_name, avatar, password 45 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 46 | ON DUPLICATE KEY UPDATE 47 | user_id = ?, 48 | username = ?, 49 | email = ?, 50 | email_hold = ?, 51 | altmail = ?, 52 | altmail_hold = ?, 53 | full_name = ?, 54 | avatar = ?, 55 | password = ? 56 | ` 57 | _, err = tx.ExecContext( 58 | ctx, 59 | qry, 60 | &id, 61 | x.Username, 62 | x.Email, 63 | x.EmailHold, 64 | x.Altmail, 65 | x.AltmailHold, 66 | x.FullName, 67 | x.Avatar, 68 | x.Password, 69 | &id, 70 | x.Username, 71 | x.Email, 72 | x.EmailHold, 73 | x.Altmail, 74 | x.AltmailHold, 75 | x.FullName, 76 | x.Avatar, 77 | x.Password, 78 | ) 79 | if err != nil { 80 | tx.Rollback() 81 | return err 82 | } 83 | 84 | qry = "INSERT into user_role (user_id, role_id) VALUES " 85 | vals, args := buildValsAndArgs(id.String(), x.RoleIds) 86 | _, err = tx.ExecContext(ctx, qry+vals, args...) 87 | if err != nil { 88 | tx.Rollback() 89 | return err 90 | } 91 | 92 | if err := tx.Commit(); err != nil { 93 | return err 94 | } 95 | 96 | // Execute another query that will return the user fields. 97 | req := pb.FndUsersReq{ 98 | RoleIds: []string{}, 99 | Email: x.Email, 100 | EmailHold: false, 101 | Altmail: "", 102 | AltmailHold: false, 103 | Limit: 1, 104 | Lapse: 0, 105 | } 106 | resp := pb.UsersResp{} 107 | 108 | if err = s.findUsers(ctx, &req, &resp); err != nil { 109 | return err 110 | } 111 | 112 | if len(resp.Items) == 0 { 113 | return errors.New("expected user to be found, but found none") 114 | } 115 | 116 | // There is only one user (Item) expected to be found. 117 | y.Item = resp.Items[0] 118 | 119 | return nil 120 | } 121 | 122 | // buildValsAndArgs enables adding all the role IDs for the user ID being added. 123 | // The values returned have bindvar pairs for each userId/roleID pair in args. 124 | func buildValsAndArgs(uid string, rids []string) (string, []interface{}) { 125 | vals := "(?, ?)" 126 | 127 | args := make([]interface{}, len(rids)*2) 128 | args[0] = uid 129 | args[1] = rids[0] 130 | 131 | for i, rid := range rids[1:] { 132 | vals += ", (?, ?)" 133 | 134 | args[i*2+2] = uid 135 | args[i*2+3] = rid 136 | } 137 | 138 | return vals, args 139 | } 140 | 141 | func (s *UserDB) deleteUser(ctx cx, sid string) error { 142 | _, err := s.db.Exec( 143 | "UPDATE user SET deleted_at = ? WHERE user_id = ?", 144 | time.Now(), 145 | sid, 146 | ) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | return nil 152 | } 153 | 154 | type userTemp struct { 155 | uid, username, email, altmail, fullName, avatar, rid, rolename string 156 | emailHold, altmailHold bool 157 | tl, tc, tu, td, tb mysql.NullTime 158 | } 159 | 160 | func (s *UserDB) findUsers(ctx cx, x *pb.FndUsersReq, y *pb.UsersResp) error { 161 | qry := ` 162 | SELECT u.user_id, u.username, u.email, u.email_hold, u.altmail, 163 | u.altmail_hold, u.full_name, u.avatar, r.role_id, r.role_name, 164 | u.last_login, u.created_at, u.updated_at, u.deleted_at, u.blocked_at 165 | FROM ( 166 | SELECT user_id, username, email, email_hold, altmail, altmail_hold, 167 | full_name, avatar, last_login, created_at, updated_at, deleted_at, 168 | blocked_at 169 | FROM user WHERE email = ? AND email_hold = ? 170 | LIMIT ? OFFSET ? 171 | ) u 172 | LEFT JOIN user_role ur 173 | ON u.user_id = ur.user_id 174 | LEFT JOIN role r 175 | ON r.role_id = ur.role_id 176 | ` 177 | 178 | rows, err := s.db.Query(qry, x.Email, x.EmailHold, x.Limit, x.Lapse) 179 | defer rows.Close() 180 | if err != nil { 181 | return err 182 | } 183 | 184 | temps := []userTemp{} 185 | 186 | for rows.Next() { 187 | u := userTemp{} 188 | 189 | err := rows.Scan( 190 | &u.uid, 191 | &u.username, 192 | &u.email, 193 | &u.emailHold, 194 | &u.altmail, 195 | &u.altmailHold, 196 | &u.fullName, 197 | &u.avatar, 198 | &u.rid, 199 | &u.rolename, 200 | &u.tl, 201 | &u.tc, 202 | &u.tu, 203 | &u.td, 204 | &u.tb, 205 | ) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | temps = append(temps, u) 211 | } 212 | if err = rows.Err(); err != nil { 213 | return err 214 | } 215 | 216 | users := squashUsers(temps) 217 | 218 | for _, u := range users { 219 | y.Items = append(y.Items, &u) 220 | } 221 | 222 | err = s.db.QueryRow( 223 | "SELECT COUNT(*) FROM user WHERE email = ? AND email_hold = ?", 224 | x.Email, 225 | x.EmailHold, 226 | ).Scan(&y.Total) 227 | if err != nil { 228 | return err 229 | } 230 | 231 | return nil 232 | } 233 | 234 | // squashUsers combines user information so there are no duplicate user IDs in slice. 235 | func squashUsers(uts []userTemp) []pb.User { 236 | var users []pb.User 237 | 238 | for _, ut := range uts { 239 | i := fndUserIndex(ut, users) 240 | 241 | if i == -1 { 242 | usr := convertUserTemp(ut) 243 | users = append(users, usr) 244 | } else { 245 | r := pb.RoleResp{ 246 | Id: ut.rid, 247 | Name: ut.rolename, 248 | } 249 | 250 | users[i].Roles = append(users[i].Roles, &r) 251 | } 252 | } 253 | 254 | return users 255 | } 256 | 257 | // userIndex gets the index of a user in []pb.User, or -1 if not found. 258 | func fndUserIndex(ut userTemp, users []pb.User) int { 259 | for i, u := range users { 260 | if u.Id == ut.uid { 261 | return i 262 | } 263 | } 264 | 265 | return -1 266 | } 267 | 268 | // convertUserTemp transfers information from userTemp to pb.User. 269 | func convertUserTemp(ut userTemp) pb.User { 270 | var u pb.User 271 | 272 | r := pb.RoleResp{ 273 | Id: ut.rid, 274 | Name: ut.rolename, 275 | } 276 | 277 | u.Id = ut.uid 278 | u.Username = ut.username 279 | u.Email = ut.email 280 | u.EmailHold = ut.emailHold 281 | u.Altmail = ut.altmail 282 | u.AltmailHold = ut.altmailHold 283 | u.FullName = ut.fullName 284 | u.Avatar = ut.avatar 285 | u.Roles = append(u.Roles, &r) 286 | u.LastLogin = asTS(ut.tl.Time, ut.tl.Valid) 287 | u.Created = asTS(ut.tc.Time, ut.tc.Valid) 288 | u.Updated = asTS(ut.tu.Time, ut.tu.Valid) 289 | u.Deleted = asTS(ut.td.Time, ut.td.Valid) 290 | 291 | return u 292 | } 293 | 294 | func (s *UserDB) upsertRole(ctx cx, sid string, x *pb.AddRoleReq, y *pb.RoleResp) error { 295 | id, ok := parseOrUID(s.ug, sid) 296 | if !ok { 297 | return fmt.Errorf("invalid uid") 298 | } 299 | 300 | qry := ` 301 | INSERT INTO role (role_id, role_name) 302 | VALUES (?, ?) 303 | ON DUPLICATE KEY UPDATE role_id = ?, role_name = ? 304 | ` 305 | 306 | _, err := s.db.Exec(qry, &id, x.Name, &id, x.Name) 307 | if err != nil { 308 | return err 309 | } 310 | 311 | // Execute another query that will return the role fields. 312 | req := pb.FndRolesReq{ 313 | RoleIds: []string{}, 314 | RoleNames: []string{x.Name}, 315 | Limit: 1, 316 | Lapse: 0, 317 | } 318 | resp := pb.RolesResp{} 319 | if err = s.findRoles(ctx, &req, &resp); err != nil { 320 | return err 321 | } 322 | 323 | if len(resp.Items) == 0 { 324 | return errors.New("upserted role not found") 325 | } 326 | 327 | // There is only one role (Item) expected to be found. 328 | y.Id = resp.Items[0].Id 329 | y.Name = resp.Items[0].Name 330 | 331 | return nil 332 | } 333 | 334 | func (s *UserDB) findRoles(ctx cx, x *pb.FndRolesReq, y *pb.RolesResp) error { 335 | req := *x 336 | roleIds := req.RoleIds 337 | roleNames := req.RoleNames 338 | 339 | if len(roleIds) == 0 { 340 | roleIds = []string{""} 341 | } 342 | if len(roleNames) == 0 { 343 | roleNames = []string{""} 344 | } 345 | 346 | qry := ` 347 | SELECT role_id, role_name 348 | FROM role 349 | WHERE role_id IN (?) OR role_name IN (?) LIMIT ? OFFSET ?; 350 | ` 351 | 352 | query, args, err := sqlx.In(qry, roleIds, roleNames, x.Limit, x.Lapse) 353 | if err != nil { 354 | return err 355 | } 356 | 357 | rows, err := s.db.Query(query, args...) 358 | if err != nil { 359 | return err 360 | } 361 | defer rows.Close() 362 | 363 | for rows.Next() { 364 | r := pb.RoleResp{} 365 | 366 | if err := rows.Scan(&r.Id, &r.Name); err != nil { 367 | return err 368 | } 369 | 370 | y.Items = append(y.Items, &r) 371 | } 372 | if err = rows.Err(); err != nil { 373 | return err 374 | } 375 | 376 | qry = "SELECT COUNT(*) FROM role WHERE role_id IN (?) OR role_name IN (?);" 377 | 378 | query, args, err = sqlx.In(qry, roleIds, roleNames) 379 | if err != nil { 380 | return err 381 | } 382 | 383 | err = s.db.QueryRow(query, args...).Scan(&y.Total) 384 | if err != nil { 385 | return err 386 | } 387 | 388 | return nil 389 | } 390 | -------------------------------------------------------------------------------- /back/tests/openbsrv/postsvc_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/OpenEugene/openboard/back/internal/pb" 9 | "google.golang.org/grpc" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | func TestPostClientServices(t *testing.T) { 14 | ctx := context.Background() 15 | conn, err := grpc.Dial(":4242", grpc.WithInsecure()) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | defer conn.Close() 20 | 21 | // Post Service Tests 22 | postClnt := pb.NewPostClient(conn) 23 | t.Run("Add type", postSvcAddAndFndTypesFn(ctx, conn, postClnt)) 24 | t.Run("Add and find posts", postSvcAddAndFndPostsFn(ctx, conn, postClnt)) 25 | t.Run("Find all posts", postSvcFindAllPosts(ctx, conn, postClnt)) 26 | t.Run("Add, find and edit post", postSvcEdtPostFn(ctx, conn, postClnt)) 27 | t.Run("Find and delete post", postSvcDelPostFn(ctx, conn, postClnt)) 28 | t.Run("Find posts by keywords", postSvcKeywordSearch(ctx, conn, postClnt)) 29 | } 30 | 31 | func postSvcAddAndFndTypesFn(ctx context.Context, conn *grpc.ClientConn, clnt pb.PostClient) func(*testing.T) { 32 | return func(t *testing.T) { 33 | ctx := context.Background() 34 | 35 | tests := []struct { 36 | wantType string 37 | addTypeReq *pb.AddTypeReq 38 | fndTypeReq *pb.FndTypesReq 39 | wantCount int 40 | }{ 41 | { 42 | "testTypeA", 43 | &pb.AddTypeReq{Name: "testTypeA"}, 44 | &pb.FndTypesReq{Limit: 100, Lapse: 0}, 45 | 1, 46 | }, 47 | { 48 | "testTypeB", 49 | &pb.AddTypeReq{Name: "testTypeB"}, 50 | &pb.FndTypesReq{Limit: 100, Lapse: 0}, 51 | 2, 52 | }, 53 | } 54 | 55 | for _, tc := range tests { 56 | r1, err := clnt.AddType(ctx, tc.addTypeReq) 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | 61 | gotType := r1.Name 62 | if gotType != tc.wantType { 63 | t.Fatalf("got: %s, want: %s", gotType, tc.wantType) 64 | } 65 | 66 | r2, err := clnt.FndTypes(ctx, tc.fndTypeReq) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | 71 | gotCount := len(r2.Items) 72 | if gotCount != tc.wantCount { 73 | t.Errorf("got %d items, want %d", gotCount, tc.wantCount) 74 | } 75 | gotType = r2.Items[tc.wantCount-1].Name 76 | if gotType != tc.wantType { 77 | t.Fatalf("got: %s, want: %s", gotType, tc.wantType) 78 | } 79 | } 80 | } 81 | } 82 | 83 | func postSvcAddAndFndPostsFn(ctx context.Context, conn *grpc.ClientConn, clnt pb.PostClient) func(*testing.T) { 84 | return func(t *testing.T) { 85 | tests := []struct { 86 | addReq *pb.AddPostReq 87 | want *pb.PostResp 88 | fndReq *pb.FndPostsReq 89 | }{ 90 | { 91 | &pb.AddPostReq{ 92 | Title: "test title postA first", 93 | Body: "test body of first post", 94 | TypeId: "2", 95 | }, 96 | &pb.PostResp{ 97 | Title: "test title postA first", 98 | Body: "test body of first post", 99 | TypeId: "2", 100 | }, 101 | &pb.FndPostsReq{Keywords: []string{ 102 | "postA", 103 | "first", 104 | "multiple", 105 | "keywords", 106 | "not", 107 | "available", 108 | "yet", 109 | }, 110 | }, 111 | }, 112 | { 113 | &pb.AddPostReq{ 114 | Title: "test title postB second", 115 | Body: "test body of second postB", 116 | TypeId: "3", 117 | }, 118 | &pb.PostResp{ 119 | Title: "test title postB second", 120 | Body: "test body of second postB", 121 | TypeId: "3", 122 | }, 123 | &pb.FndPostsReq{Keywords: []string{"postB"}}, 124 | }, 125 | } 126 | 127 | for _, tc := range tests { 128 | r, err := clnt.AddPost(ctx, tc.addReq) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | got := r 134 | wantPostID := got.Id 135 | 136 | // Unset fields that aren't being tested. 137 | unsetUntestedFields(got) 138 | 139 | if !proto.Equal(got, tc.want) { 140 | t.Errorf("got: %#v, want: %#v", got, tc.want) 141 | } 142 | 143 | gotPostID, err := postSvcFndPost(ctx, conn, clnt, tc.fndReq) 144 | if err != nil { 145 | t.Error(err) 146 | } 147 | 148 | if gotPostID != wantPostID { 149 | t.Fatalf("got: %s, want: %s", gotPostID, wantPostID) 150 | } 151 | } 152 | } 153 | } 154 | 155 | func postSvcEdtPostFn(ctx context.Context, conn *grpc.ClientConn, clnt pb.PostClient) func(*testing.T) { 156 | return func(t *testing.T) { 157 | addReq := &pb.AddPostReq{ 158 | Title: "Post C", 159 | Body: "This is post C.", 160 | TypeId: "2", 161 | } 162 | _, err := clnt.AddPost(ctx, addReq) 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | 167 | fndReq := &pb.FndPostsReq{Keywords: []string{"post C"}} 168 | postID, err := postSvcFndPost(ctx, conn, clnt, fndReq) 169 | if err != nil { 170 | t.Error(err) 171 | } 172 | 173 | editReq := &pb.AddPostReq{ 174 | Title: "Post C (edited)", 175 | Body: "This is post C after edits.", 176 | TypeId: "2", 177 | } 178 | 179 | ovrReq := &pb.OvrPostReq{Id: postID, Req: editReq} 180 | r, err := clnt.OvrPost(ctx, ovrReq) 181 | if err != nil { 182 | t.Error(err) 183 | } 184 | 185 | want := &pb.PostResp{ 186 | Title: editReq.Title, 187 | Body: editReq.Body, 188 | TypeId: editReq.TypeId, 189 | } 190 | 191 | got := r 192 | 193 | unsetUntestedFields(got) 194 | 195 | if !proto.Equal(got, want) { 196 | t.Fatalf("got: %#v, want: %#v", got, want) 197 | } 198 | } 199 | } 200 | 201 | func postSvcDelPostFn(ctx context.Context, conn *grpc.ClientConn, clnt pb.PostClient) func(*testing.T) { 202 | return func(t *testing.T) { 203 | addReq := &pb.AddPostReq{ 204 | Title: "test title postD first", 205 | Body: "test body of fourth post", 206 | TypeId: "4", 207 | } 208 | 209 | _, err := clnt.AddPost(ctx, addReq) 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | 214 | fndReq := &pb.FndPostsReq{Keywords: []string{"postD"}} 215 | 216 | fndResp, err := clnt.FndPosts(ctx, fndReq) 217 | if err != nil { 218 | t.Error(err) 219 | } 220 | 221 | if fndResp.Posts[0].Deleted != nil { 222 | t.Fatalf( 223 | "expected post %s deleted_at to be nil, got %v", 224 | fndResp.Posts[0].Id, 225 | fndResp.Posts[0].Deleted, 226 | ) 227 | t.Fail() 228 | } 229 | 230 | _, err = clnt.RmvPost(ctx, &pb.RmvPostReq{Id: fndResp.Posts[0].Id}) 231 | if err != nil { 232 | t.Error(err) 233 | } 234 | 235 | fndResp, err = clnt.FndPosts(ctx, fndReq) 236 | if err != nil { 237 | t.Error(err) 238 | } 239 | 240 | if fndResp.Posts[0].Deleted == nil { 241 | t.Fatalf("expected post %s deleted_at to not be nil", fndResp.Posts[0].Id) 242 | t.Fail() 243 | } 244 | } 245 | } 246 | 247 | func postSvcFndPost(ctx context.Context, conn *grpc.ClientConn, clnt pb.PostClient, req *pb.FndPostsReq) (string, error) { 248 | postID, err := clnt.FndPosts(ctx, req) 249 | if err != nil { 250 | return "", fmt.Errorf("Unable to find post: %w", err) 251 | } 252 | 253 | if len(postID.Posts) > 0 { 254 | return postID.Posts[0].Id, nil 255 | } 256 | return "", nil 257 | } 258 | 259 | func postsContain(resp *pb.PostsResp, post *pb.PostResp) bool { 260 | for _, j := range resp.Posts { 261 | if j.Title == post.Title && j.Body == post.Body && j.TypeId == post.TypeId { 262 | return true 263 | } 264 | } 265 | return false 266 | } 267 | 268 | func postSvcFindAllPosts(ctx context.Context, conn *grpc.ClientConn, clnt pb.PostClient) func(*testing.T) { 269 | return func(t *testing.T) { 270 | tests := []struct { 271 | fndReq *pb.FndPostsReq 272 | want *pb.PostsResp 273 | }{ 274 | { 275 | &pb.FndPostsReq{Keywords: []string{}}, 276 | &pb.PostsResp{ 277 | Posts: []*pb.PostResp{ 278 | { 279 | Title: "test title postA first", 280 | Body: "test body of first post", 281 | TypeId: "2", 282 | }, 283 | { 284 | Title: "test title postB second", 285 | Body: "test body of second postB", 286 | TypeId: "3", 287 | }, 288 | }, 289 | }, 290 | }, 291 | } 292 | 293 | for _, tc := range tests { 294 | resp, err := clnt.FndPosts(ctx, tc.fndReq) 295 | if err != nil { 296 | t.Errorf("unexpected error: %v", err) 297 | } 298 | 299 | if len(tc.want.Posts) != len(resp.Posts) { 300 | t.Errorf("mismatch between response length and post length, "+ 301 | "want: %d got: %d", len(tc.want.Posts), len(resp.Posts)) 302 | } 303 | 304 | for _, post := range tc.want.Posts { 305 | if !postsContain(resp, post) { 306 | t.Errorf("couldn't find post with title: %s", post.Title) 307 | } 308 | } 309 | } 310 | } 311 | } 312 | 313 | func postSvcKeywordSearch(ctx context.Context, conn *grpc.ClientConn, clnt pb.PostClient) func(*testing.T) { 314 | return func(t *testing.T) { 315 | tests := []struct { 316 | name string 317 | fndReq *pb.FndPostsReq 318 | want *pb.PostsResp 319 | }{ 320 | { 321 | "not find non-matching posts", 322 | &pb.FndPostsReq{Keywords: []string{"randomgiberish"}}, 323 | &pb.PostsResp{ 324 | Posts: []*pb.PostResp{}, 325 | }, 326 | }, 327 | { 328 | "find posts by several keywords", 329 | &pb.FndPostsReq{Keywords: []string{"postB", "postD"}}, 330 | &pb.PostsResp{ 331 | Posts: []*pb.PostResp{ 332 | { 333 | Title: "test title postB second", 334 | Body: "test body of second postB", 335 | TypeId: "3", 336 | }, 337 | { 338 | Title: "test title postD first", 339 | Body: "test body of fourth post", 340 | TypeId: "4", 341 | }, 342 | }, 343 | }, 344 | }, 345 | { 346 | "find keyword within parenthesis", 347 | &pb.FndPostsReq{Keywords: []string{"edited"}}, 348 | &pb.PostsResp{ 349 | Posts: []*pb.PostResp{ 350 | { 351 | Title: "Post C (edited)", 352 | Body: "This is post C after edits.", 353 | TypeId: "2", 354 | }, 355 | }, 356 | }, 357 | }, 358 | } 359 | 360 | for _, tc := range tests { 361 | resp, err := clnt.FndPosts(ctx, tc.fndReq) 362 | if err != nil { 363 | t.Errorf("%s: unexpected error: %v", tc.name, err) 364 | } 365 | 366 | if len(tc.want.Posts) != len(resp.Posts) { 367 | t.Errorf("%s: mismatch between response length and post length, "+ 368 | "want: %d got: %d", tc.name, len(tc.want.Posts), len(resp.Posts)) 369 | } 370 | 371 | for _, post := range tc.want.Posts { 372 | if !postsContain(resp, post) { 373 | t.Errorf("%s: couldn't find post with title: %s", tc.name, post.Title) 374 | } 375 | } 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /back/internal/pb/post_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package pb 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // PostClient is the client API for Post service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type PostClient interface { 21 | AddType(ctx context.Context, in *AddTypeReq, opts ...grpc.CallOption) (*TypeResp, error) 22 | FndTypes(ctx context.Context, in *FndTypesReq, opts ...grpc.CallOption) (*TypesResp, error) 23 | AddPost(ctx context.Context, in *AddPostReq, opts ...grpc.CallOption) (*PostResp, error) 24 | FndPosts(ctx context.Context, in *FndPostsReq, opts ...grpc.CallOption) (*PostsResp, error) 25 | OvrPost(ctx context.Context, in *OvrPostReq, opts ...grpc.CallOption) (*PostResp, error) 26 | RmvPost(ctx context.Context, in *RmvPostReq, opts ...grpc.CallOption) (*RmvPostResp, error) 27 | } 28 | 29 | type postClient struct { 30 | cc grpc.ClientConnInterface 31 | } 32 | 33 | func NewPostClient(cc grpc.ClientConnInterface) PostClient { 34 | return &postClient{cc} 35 | } 36 | 37 | func (c *postClient) AddType(ctx context.Context, in *AddTypeReq, opts ...grpc.CallOption) (*TypeResp, error) { 38 | out := new(TypeResp) 39 | err := c.cc.Invoke(ctx, "/pb.Post/AddType", in, out, opts...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return out, nil 44 | } 45 | 46 | func (c *postClient) FndTypes(ctx context.Context, in *FndTypesReq, opts ...grpc.CallOption) (*TypesResp, error) { 47 | out := new(TypesResp) 48 | err := c.cc.Invoke(ctx, "/pb.Post/FndTypes", in, out, opts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | 55 | func (c *postClient) AddPost(ctx context.Context, in *AddPostReq, opts ...grpc.CallOption) (*PostResp, error) { 56 | out := new(PostResp) 57 | err := c.cc.Invoke(ctx, "/pb.Post/AddPost", in, out, opts...) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return out, nil 62 | } 63 | 64 | func (c *postClient) FndPosts(ctx context.Context, in *FndPostsReq, opts ...grpc.CallOption) (*PostsResp, error) { 65 | out := new(PostsResp) 66 | err := c.cc.Invoke(ctx, "/pb.Post/FndPosts", in, out, opts...) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return out, nil 71 | } 72 | 73 | func (c *postClient) OvrPost(ctx context.Context, in *OvrPostReq, opts ...grpc.CallOption) (*PostResp, error) { 74 | out := new(PostResp) 75 | err := c.cc.Invoke(ctx, "/pb.Post/OvrPost", in, out, opts...) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return out, nil 80 | } 81 | 82 | func (c *postClient) RmvPost(ctx context.Context, in *RmvPostReq, opts ...grpc.CallOption) (*RmvPostResp, error) { 83 | out := new(RmvPostResp) 84 | err := c.cc.Invoke(ctx, "/pb.Post/RmvPost", in, out, opts...) 85 | if err != nil { 86 | return nil, err 87 | } 88 | return out, nil 89 | } 90 | 91 | // PostServer is the server API for Post service. 92 | // All implementations should embed UnimplementedPostServer 93 | // for forward compatibility 94 | type PostServer interface { 95 | AddType(context.Context, *AddTypeReq) (*TypeResp, error) 96 | FndTypes(context.Context, *FndTypesReq) (*TypesResp, error) 97 | AddPost(context.Context, *AddPostReq) (*PostResp, error) 98 | FndPosts(context.Context, *FndPostsReq) (*PostsResp, error) 99 | OvrPost(context.Context, *OvrPostReq) (*PostResp, error) 100 | RmvPost(context.Context, *RmvPostReq) (*RmvPostResp, error) 101 | } 102 | 103 | // UnimplementedPostServer should be embedded to have forward compatible implementations. 104 | type UnimplementedPostServer struct { 105 | } 106 | 107 | func (UnimplementedPostServer) AddType(context.Context, *AddTypeReq) (*TypeResp, error) { 108 | return nil, status.Errorf(codes.Unimplemented, "method AddType not implemented") 109 | } 110 | func (UnimplementedPostServer) FndTypes(context.Context, *FndTypesReq) (*TypesResp, error) { 111 | return nil, status.Errorf(codes.Unimplemented, "method FndTypes not implemented") 112 | } 113 | func (UnimplementedPostServer) AddPost(context.Context, *AddPostReq) (*PostResp, error) { 114 | return nil, status.Errorf(codes.Unimplemented, "method AddPost not implemented") 115 | } 116 | func (UnimplementedPostServer) FndPosts(context.Context, *FndPostsReq) (*PostsResp, error) { 117 | return nil, status.Errorf(codes.Unimplemented, "method FndPosts not implemented") 118 | } 119 | func (UnimplementedPostServer) OvrPost(context.Context, *OvrPostReq) (*PostResp, error) { 120 | return nil, status.Errorf(codes.Unimplemented, "method OvrPost not implemented") 121 | } 122 | func (UnimplementedPostServer) RmvPost(context.Context, *RmvPostReq) (*RmvPostResp, error) { 123 | return nil, status.Errorf(codes.Unimplemented, "method RmvPost not implemented") 124 | } 125 | 126 | // UnsafePostServer may be embedded to opt out of forward compatibility for this service. 127 | // Use of this interface is not recommended, as added methods to PostServer will 128 | // result in compilation errors. 129 | type UnsafePostServer interface { 130 | mustEmbedUnimplementedPostServer() 131 | } 132 | 133 | func RegisterPostServer(s grpc.ServiceRegistrar, srv PostServer) { 134 | s.RegisterService(&Post_ServiceDesc, srv) 135 | } 136 | 137 | func _Post_AddType_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 138 | in := new(AddTypeReq) 139 | if err := dec(in); err != nil { 140 | return nil, err 141 | } 142 | if interceptor == nil { 143 | return srv.(PostServer).AddType(ctx, in) 144 | } 145 | info := &grpc.UnaryServerInfo{ 146 | Server: srv, 147 | FullMethod: "/pb.Post/AddType", 148 | } 149 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 150 | return srv.(PostServer).AddType(ctx, req.(*AddTypeReq)) 151 | } 152 | return interceptor(ctx, in, info, handler) 153 | } 154 | 155 | func _Post_FndTypes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 156 | in := new(FndTypesReq) 157 | if err := dec(in); err != nil { 158 | return nil, err 159 | } 160 | if interceptor == nil { 161 | return srv.(PostServer).FndTypes(ctx, in) 162 | } 163 | info := &grpc.UnaryServerInfo{ 164 | Server: srv, 165 | FullMethod: "/pb.Post/FndTypes", 166 | } 167 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 168 | return srv.(PostServer).FndTypes(ctx, req.(*FndTypesReq)) 169 | } 170 | return interceptor(ctx, in, info, handler) 171 | } 172 | 173 | func _Post_AddPost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 174 | in := new(AddPostReq) 175 | if err := dec(in); err != nil { 176 | return nil, err 177 | } 178 | if interceptor == nil { 179 | return srv.(PostServer).AddPost(ctx, in) 180 | } 181 | info := &grpc.UnaryServerInfo{ 182 | Server: srv, 183 | FullMethod: "/pb.Post/AddPost", 184 | } 185 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 186 | return srv.(PostServer).AddPost(ctx, req.(*AddPostReq)) 187 | } 188 | return interceptor(ctx, in, info, handler) 189 | } 190 | 191 | func _Post_FndPosts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 192 | in := new(FndPostsReq) 193 | if err := dec(in); err != nil { 194 | return nil, err 195 | } 196 | if interceptor == nil { 197 | return srv.(PostServer).FndPosts(ctx, in) 198 | } 199 | info := &grpc.UnaryServerInfo{ 200 | Server: srv, 201 | FullMethod: "/pb.Post/FndPosts", 202 | } 203 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 204 | return srv.(PostServer).FndPosts(ctx, req.(*FndPostsReq)) 205 | } 206 | return interceptor(ctx, in, info, handler) 207 | } 208 | 209 | func _Post_OvrPost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 210 | in := new(OvrPostReq) 211 | if err := dec(in); err != nil { 212 | return nil, err 213 | } 214 | if interceptor == nil { 215 | return srv.(PostServer).OvrPost(ctx, in) 216 | } 217 | info := &grpc.UnaryServerInfo{ 218 | Server: srv, 219 | FullMethod: "/pb.Post/OvrPost", 220 | } 221 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 222 | return srv.(PostServer).OvrPost(ctx, req.(*OvrPostReq)) 223 | } 224 | return interceptor(ctx, in, info, handler) 225 | } 226 | 227 | func _Post_RmvPost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 228 | in := new(RmvPostReq) 229 | if err := dec(in); err != nil { 230 | return nil, err 231 | } 232 | if interceptor == nil { 233 | return srv.(PostServer).RmvPost(ctx, in) 234 | } 235 | info := &grpc.UnaryServerInfo{ 236 | Server: srv, 237 | FullMethod: "/pb.Post/RmvPost", 238 | } 239 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 240 | return srv.(PostServer).RmvPost(ctx, req.(*RmvPostReq)) 241 | } 242 | return interceptor(ctx, in, info, handler) 243 | } 244 | 245 | // Post_ServiceDesc is the grpc.ServiceDesc for Post service. 246 | // It's only intended for direct use with grpc.RegisterService, 247 | // and not to be introspected or modified (even as a copy) 248 | var Post_ServiceDesc = grpc.ServiceDesc{ 249 | ServiceName: "pb.Post", 250 | HandlerType: (*PostServer)(nil), 251 | Methods: []grpc.MethodDesc{ 252 | { 253 | MethodName: "AddType", 254 | Handler: _Post_AddType_Handler, 255 | }, 256 | { 257 | MethodName: "FndTypes", 258 | Handler: _Post_FndTypes_Handler, 259 | }, 260 | { 261 | MethodName: "AddPost", 262 | Handler: _Post_AddPost_Handler, 263 | }, 264 | { 265 | MethodName: "FndPosts", 266 | Handler: _Post_FndPosts_Handler, 267 | }, 268 | { 269 | MethodName: "OvrPost", 270 | Handler: _Post_OvrPost_Handler, 271 | }, 272 | { 273 | MethodName: "RmvPost", 274 | Handler: _Post_RmvPost_Handler, 275 | }, 276 | }, 277 | Streams: []grpc.StreamDesc{}, 278 | Metadata: "post.proto", 279 | } 280 | -------------------------------------------------------------------------------- /back/internal/pb/user_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package pb 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // UserSvcClient is the client API for UserSvc service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type UserSvcClient interface { 21 | AddRole(ctx context.Context, in *AddRoleReq, opts ...grpc.CallOption) (*RoleResp, error) 22 | FndRoles(ctx context.Context, in *FndRolesReq, opts ...grpc.CallOption) (*RolesResp, error) 23 | AddUser(ctx context.Context, in *AddUserReq, opts ...grpc.CallOption) (*UserResp, error) 24 | OvrUser(ctx context.Context, in *OvrUserReq, opts ...grpc.CallOption) (*UserResp, error) 25 | FndUsers(ctx context.Context, in *FndUsersReq, opts ...grpc.CallOption) (*UsersResp, error) 26 | RmvUser(ctx context.Context, in *RmvUserReq, opts ...grpc.CallOption) (*RmvUserResp, error) 27 | } 28 | 29 | type userSvcClient struct { 30 | cc grpc.ClientConnInterface 31 | } 32 | 33 | func NewUserSvcClient(cc grpc.ClientConnInterface) UserSvcClient { 34 | return &userSvcClient{cc} 35 | } 36 | 37 | func (c *userSvcClient) AddRole(ctx context.Context, in *AddRoleReq, opts ...grpc.CallOption) (*RoleResp, error) { 38 | out := new(RoleResp) 39 | err := c.cc.Invoke(ctx, "/pb.UserSvc/AddRole", in, out, opts...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return out, nil 44 | } 45 | 46 | func (c *userSvcClient) FndRoles(ctx context.Context, in *FndRolesReq, opts ...grpc.CallOption) (*RolesResp, error) { 47 | out := new(RolesResp) 48 | err := c.cc.Invoke(ctx, "/pb.UserSvc/FndRoles", in, out, opts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | 55 | func (c *userSvcClient) AddUser(ctx context.Context, in *AddUserReq, opts ...grpc.CallOption) (*UserResp, error) { 56 | out := new(UserResp) 57 | err := c.cc.Invoke(ctx, "/pb.UserSvc/AddUser", in, out, opts...) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return out, nil 62 | } 63 | 64 | func (c *userSvcClient) OvrUser(ctx context.Context, in *OvrUserReq, opts ...grpc.CallOption) (*UserResp, error) { 65 | out := new(UserResp) 66 | err := c.cc.Invoke(ctx, "/pb.UserSvc/OvrUser", in, out, opts...) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return out, nil 71 | } 72 | 73 | func (c *userSvcClient) FndUsers(ctx context.Context, in *FndUsersReq, opts ...grpc.CallOption) (*UsersResp, error) { 74 | out := new(UsersResp) 75 | err := c.cc.Invoke(ctx, "/pb.UserSvc/FndUsers", in, out, opts...) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return out, nil 80 | } 81 | 82 | func (c *userSvcClient) RmvUser(ctx context.Context, in *RmvUserReq, opts ...grpc.CallOption) (*RmvUserResp, error) { 83 | out := new(RmvUserResp) 84 | err := c.cc.Invoke(ctx, "/pb.UserSvc/RmvUser", in, out, opts...) 85 | if err != nil { 86 | return nil, err 87 | } 88 | return out, nil 89 | } 90 | 91 | // UserSvcServer is the server API for UserSvc service. 92 | // All implementations should embed UnimplementedUserSvcServer 93 | // for forward compatibility 94 | type UserSvcServer interface { 95 | AddRole(context.Context, *AddRoleReq) (*RoleResp, error) 96 | FndRoles(context.Context, *FndRolesReq) (*RolesResp, error) 97 | AddUser(context.Context, *AddUserReq) (*UserResp, error) 98 | OvrUser(context.Context, *OvrUserReq) (*UserResp, error) 99 | FndUsers(context.Context, *FndUsersReq) (*UsersResp, error) 100 | RmvUser(context.Context, *RmvUserReq) (*RmvUserResp, error) 101 | } 102 | 103 | // UnimplementedUserSvcServer should be embedded to have forward compatible implementations. 104 | type UnimplementedUserSvcServer struct { 105 | } 106 | 107 | func (UnimplementedUserSvcServer) AddRole(context.Context, *AddRoleReq) (*RoleResp, error) { 108 | return nil, status.Errorf(codes.Unimplemented, "method AddRole not implemented") 109 | } 110 | func (UnimplementedUserSvcServer) FndRoles(context.Context, *FndRolesReq) (*RolesResp, error) { 111 | return nil, status.Errorf(codes.Unimplemented, "method FndRoles not implemented") 112 | } 113 | func (UnimplementedUserSvcServer) AddUser(context.Context, *AddUserReq) (*UserResp, error) { 114 | return nil, status.Errorf(codes.Unimplemented, "method AddUser not implemented") 115 | } 116 | func (UnimplementedUserSvcServer) OvrUser(context.Context, *OvrUserReq) (*UserResp, error) { 117 | return nil, status.Errorf(codes.Unimplemented, "method OvrUser not implemented") 118 | } 119 | func (UnimplementedUserSvcServer) FndUsers(context.Context, *FndUsersReq) (*UsersResp, error) { 120 | return nil, status.Errorf(codes.Unimplemented, "method FndUsers not implemented") 121 | } 122 | func (UnimplementedUserSvcServer) RmvUser(context.Context, *RmvUserReq) (*RmvUserResp, error) { 123 | return nil, status.Errorf(codes.Unimplemented, "method RmvUser not implemented") 124 | } 125 | 126 | // UnsafeUserSvcServer may be embedded to opt out of forward compatibility for this service. 127 | // Use of this interface is not recommended, as added methods to UserSvcServer will 128 | // result in compilation errors. 129 | type UnsafeUserSvcServer interface { 130 | mustEmbedUnimplementedUserSvcServer() 131 | } 132 | 133 | func RegisterUserSvcServer(s grpc.ServiceRegistrar, srv UserSvcServer) { 134 | s.RegisterService(&UserSvc_ServiceDesc, srv) 135 | } 136 | 137 | func _UserSvc_AddRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 138 | in := new(AddRoleReq) 139 | if err := dec(in); err != nil { 140 | return nil, err 141 | } 142 | if interceptor == nil { 143 | return srv.(UserSvcServer).AddRole(ctx, in) 144 | } 145 | info := &grpc.UnaryServerInfo{ 146 | Server: srv, 147 | FullMethod: "/pb.UserSvc/AddRole", 148 | } 149 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 150 | return srv.(UserSvcServer).AddRole(ctx, req.(*AddRoleReq)) 151 | } 152 | return interceptor(ctx, in, info, handler) 153 | } 154 | 155 | func _UserSvc_FndRoles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 156 | in := new(FndRolesReq) 157 | if err := dec(in); err != nil { 158 | return nil, err 159 | } 160 | if interceptor == nil { 161 | return srv.(UserSvcServer).FndRoles(ctx, in) 162 | } 163 | info := &grpc.UnaryServerInfo{ 164 | Server: srv, 165 | FullMethod: "/pb.UserSvc/FndRoles", 166 | } 167 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 168 | return srv.(UserSvcServer).FndRoles(ctx, req.(*FndRolesReq)) 169 | } 170 | return interceptor(ctx, in, info, handler) 171 | } 172 | 173 | func _UserSvc_AddUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 174 | in := new(AddUserReq) 175 | if err := dec(in); err != nil { 176 | return nil, err 177 | } 178 | if interceptor == nil { 179 | return srv.(UserSvcServer).AddUser(ctx, in) 180 | } 181 | info := &grpc.UnaryServerInfo{ 182 | Server: srv, 183 | FullMethod: "/pb.UserSvc/AddUser", 184 | } 185 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 186 | return srv.(UserSvcServer).AddUser(ctx, req.(*AddUserReq)) 187 | } 188 | return interceptor(ctx, in, info, handler) 189 | } 190 | 191 | func _UserSvc_OvrUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 192 | in := new(OvrUserReq) 193 | if err := dec(in); err != nil { 194 | return nil, err 195 | } 196 | if interceptor == nil { 197 | return srv.(UserSvcServer).OvrUser(ctx, in) 198 | } 199 | info := &grpc.UnaryServerInfo{ 200 | Server: srv, 201 | FullMethod: "/pb.UserSvc/OvrUser", 202 | } 203 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 204 | return srv.(UserSvcServer).OvrUser(ctx, req.(*OvrUserReq)) 205 | } 206 | return interceptor(ctx, in, info, handler) 207 | } 208 | 209 | func _UserSvc_FndUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 210 | in := new(FndUsersReq) 211 | if err := dec(in); err != nil { 212 | return nil, err 213 | } 214 | if interceptor == nil { 215 | return srv.(UserSvcServer).FndUsers(ctx, in) 216 | } 217 | info := &grpc.UnaryServerInfo{ 218 | Server: srv, 219 | FullMethod: "/pb.UserSvc/FndUsers", 220 | } 221 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 222 | return srv.(UserSvcServer).FndUsers(ctx, req.(*FndUsersReq)) 223 | } 224 | return interceptor(ctx, in, info, handler) 225 | } 226 | 227 | func _UserSvc_RmvUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 228 | in := new(RmvUserReq) 229 | if err := dec(in); err != nil { 230 | return nil, err 231 | } 232 | if interceptor == nil { 233 | return srv.(UserSvcServer).RmvUser(ctx, in) 234 | } 235 | info := &grpc.UnaryServerInfo{ 236 | Server: srv, 237 | FullMethod: "/pb.UserSvc/RmvUser", 238 | } 239 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 240 | return srv.(UserSvcServer).RmvUser(ctx, req.(*RmvUserReq)) 241 | } 242 | return interceptor(ctx, in, info, handler) 243 | } 244 | 245 | // UserSvc_ServiceDesc is the grpc.ServiceDesc for UserSvc service. 246 | // It's only intended for direct use with grpc.RegisterService, 247 | // and not to be introspected or modified (even as a copy) 248 | var UserSvc_ServiceDesc = grpc.ServiceDesc{ 249 | ServiceName: "pb.UserSvc", 250 | HandlerType: (*UserSvcServer)(nil), 251 | Methods: []grpc.MethodDesc{ 252 | { 253 | MethodName: "AddRole", 254 | Handler: _UserSvc_AddRole_Handler, 255 | }, 256 | { 257 | MethodName: "FndRoles", 258 | Handler: _UserSvc_FndRoles_Handler, 259 | }, 260 | { 261 | MethodName: "AddUser", 262 | Handler: _UserSvc_AddUser_Handler, 263 | }, 264 | { 265 | MethodName: "OvrUser", 266 | Handler: _UserSvc_OvrUser_Handler, 267 | }, 268 | { 269 | MethodName: "FndUsers", 270 | Handler: _UserSvc_FndUsers_Handler, 271 | }, 272 | { 273 | MethodName: "RmvUser", 274 | Handler: _UserSvc_RmvUser_Handler, 275 | }, 276 | }, 277 | Streams: []grpc.StreamDesc{}, 278 | Metadata: "user.proto", 279 | } 280 | -------------------------------------------------------------------------------- /msgs/proto/include/googleapis/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------