├── frontend ├── src │ ├── assets │ │ ├── .gitkeep │ │ ├── cover.jpg │ │ ├── heart.png │ │ ├── image.jpg │ │ ├── image.png │ │ └── report.pdf │ ├── app │ │ ├── containers │ │ │ ├── about │ │ │ │ ├── about.container.scss │ │ │ │ ├── index.ts │ │ │ │ ├── about.container.html │ │ │ │ └── about.container.ts │ │ │ ├── howto │ │ │ │ ├── howto.container.scss │ │ │ │ ├── index.ts │ │ │ │ ├── howto.container.html │ │ │ │ └── howto.container.ts │ │ │ ├── login │ │ │ │ ├── login.container.scss │ │ │ │ ├── index.ts │ │ │ │ ├── login.container.html │ │ │ │ └── login.container.ts │ │ │ ├── signup │ │ │ │ ├── signup.component.scss │ │ │ │ ├── index.ts │ │ │ │ ├── signup.component.html │ │ │ │ └── signup.component.ts │ │ │ ├── home │ │ │ │ ├── index.ts │ │ │ │ └── home.component.ts │ │ │ ├── main │ │ │ │ ├── index.ts │ │ │ │ ├── main.component.scss │ │ │ │ ├── main.component.html │ │ │ │ └── main.component.ts │ │ │ ├── how-it-works │ │ │ │ ├── index.ts │ │ │ │ ├── how-it-works.component.scss │ │ │ │ ├── how-it-works.component.html │ │ │ │ └── how-it-works.component.ts │ │ │ └── index.ts │ │ ├── components │ │ │ ├── about │ │ │ │ ├── index.ts │ │ │ │ ├── about.component.scss │ │ │ │ ├── about.component.ts │ │ │ │ └── about.component.html │ │ │ ├── home │ │ │ │ ├── index.ts │ │ │ │ ├── home.component.scss │ │ │ │ ├── home.component.ts │ │ │ │ └── home.component.html │ │ │ ├── howto │ │ │ │ ├── index.ts │ │ │ │ ├── howto.component.scss │ │ │ │ ├── howto.component.ts │ │ │ │ └── howto.component.html │ │ │ ├── login │ │ │ │ ├── index.ts │ │ │ │ ├── login.component.scss │ │ │ │ ├── login.component.ts │ │ │ │ └── login.component.html │ │ │ ├── search │ │ │ │ ├── index.ts │ │ │ │ ├── search.component.html │ │ │ │ ├── search.component.css │ │ │ │ └── search.component.ts │ │ │ ├── signup │ │ │ │ ├── index.ts │ │ │ │ ├── signup.component.scss │ │ │ │ ├── signup.component.html │ │ │ │ └── signup.component.ts │ │ │ ├── results │ │ │ │ ├── index.ts │ │ │ │ ├── results.component.scss │ │ │ │ ├── results.component.ts │ │ │ │ └── results.component.html │ │ │ ├── student │ │ │ │ ├── index.ts │ │ │ │ ├── student.component.html │ │ │ │ ├── student.component.css │ │ │ │ └── student.component.ts │ │ │ ├── countdown │ │ │ │ ├── index.ts │ │ │ │ ├── countdown.component.scss │ │ │ │ ├── countdown.component.html │ │ │ │ └── countdown.component.ts │ │ │ └── index.ts │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.component.html │ │ ├── guards │ │ │ ├── logged-in.guard.ts │ │ │ └── logged-out.guard.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.spec.ts │ │ ├── app.module.ts │ │ ├── crypto.ts │ │ └── services │ │ │ └── main.service.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── styles.scss │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── main.ts │ ├── index.html │ ├── ngsw-config.json │ ├── test.ts │ └── polyfills.ts ├── .dockerignore ├── proxy.conf.json ├── e2e │ ├── app.po.ts │ ├── tsconfig.e2e.json │ └── app.e2e-spec.ts ├── .editorconfig ├── nginx.conf ├── tsconfig.json ├── Dockerfile ├── .gitignore ├── protractor.conf.js ├── karma.conf.js ├── README.md ├── .angular-cli.json ├── package.json └── tslint.json ├── service ├── results │ ├── .gitignore │ ├── .dockerignore │ ├── mongo.go │ ├── Gopkg.lock │ ├── config.go │ ├── Gopkg.toml │ ├── Dockerfile │ ├── user.go │ └── results.go ├── signup │ ├── .dockerignore │ ├── .gitignore │ ├── mongo.go │ ├── Gopkg.lock │ ├── Gopkg.toml │ ├── Dockerfile │ ├── config.go │ ├── signup.go │ ├── user.go │ └── routines.go └── nginx │ ├── Dockerfile │ └── nginx.prod.conf ├── cover.jpg ├── report ├── .gitignore ├── puppylove.pdf └── puppylove.tex ├── .dockerignore ├── scripts ├── mongodump │ └── puppy │ │ ├── user.bson │ │ ├── declare.bson │ │ ├── heart.bson │ │ ├── heart.metadata.json │ │ ├── user.metadata.json │ │ └── declare.metadata.json ├── compute.sh ├── logsof.sh ├── smallcompute.sh ├── newuser.sh ├── login.sh ├── script.js ├── allusers.py └── package-lock.json ├── .gitignore ├── .backend.env ├── models ├── hearts.go ├── declare.go └── user.go ├── db └── mongo.go ├── utils ├── http.go └── utils.go ├── Gopkg.toml ├── Dockerfile ├── docker-compose.yml ├── main.go ├── LICENSE ├── puppy.nginx.conf ├── controllers ├── declare.go ├── heart.go ├── list.go ├── session.go ├── stats.go └── user.go ├── config └── config.go ├── router └── router.go ├── Gopkg.lock └── README.md /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service/results/.gitignore: -------------------------------------------------------------------------------- 1 | /results 2 | vendor/ 3 | -------------------------------------------------------------------------------- /frontend/src/app/containers/about/about.container.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/containers/howto/howto.container.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/containers/login/login.container.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/containers/signup/signup.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /service/results/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | vendor 3 | -------------------------------------------------------------------------------- /service/signup/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | vendor 3 | -------------------------------------------------------------------------------- /service/signup/.gitignore: -------------------------------------------------------------------------------- 1 | signup 2 | vendor/ 3 | .envfile 4 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/cover.jpg -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /report/.gitignore: -------------------------------------------------------------------------------- 1 | *.aux 2 | *.log 3 | *.out 4 | *.ps 5 | /auto 6 | /auto/* 7 | -------------------------------------------------------------------------------- /frontend/src/app/components/about/index.ts: -------------------------------------------------------------------------------- 1 | export * from './about.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/components/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/components/howto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './howto.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/components/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/components/search/index.ts: -------------------------------------------------------------------------------- 1 | export * from './search.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/components/signup/index.ts: -------------------------------------------------------------------------------- 1 | export * from './signup.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/about/index.ts: -------------------------------------------------------------------------------- 1 | export * from './about.container'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/howto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './howto.container'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.container'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/main/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/signup/index.ts: -------------------------------------------------------------------------------- 1 | export * from './signup.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/components/results/index.ts: -------------------------------------------------------------------------------- 1 | export * from './results.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/components/student/index.ts: -------------------------------------------------------------------------------- 1 | export * from './student.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/about/about.container.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/howto/howto.container.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/app/components/countdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './countdown.component'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/how-it-works/index.ts: -------------------------------------------------------------------------------- 1 | export * from './how-it-works.component'; 2 | -------------------------------------------------------------------------------- /report/puppylove.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/report/puppylove.pdf -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | frontend/ 2 | vendor/ 3 | Dockerfile 4 | .dockerignore 5 | cover.jpg 6 | service/ 7 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/how-it-works/how-it-works.component.scss: -------------------------------------------------------------------------------- 1 | [mat-button] { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/assets/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/cover.jpg -------------------------------------------------------------------------------- /frontend/src/assets/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/heart.png -------------------------------------------------------------------------------- /frontend/src/assets/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/image.jpg -------------------------------------------------------------------------------- /frontend/src/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/image.png -------------------------------------------------------------------------------- /frontend/src/assets/report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/report.pdf -------------------------------------------------------------------------------- /scripts/mongodump/puppy/user.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/scripts/mongodump/puppy/user.bson -------------------------------------------------------------------------------- /scripts/compute.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | http -v --timeout=24000 'localhost:3000/admin/compute/prepare/1' "$CADMIN" 4 | -------------------------------------------------------------------------------- /scripts/mongodump/puppy/declare.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/scripts/mongodump/puppy/declare.bson -------------------------------------------------------------------------------- /scripts/mongodump/puppy/heart.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/scripts/mongodump/puppy/heart.bson -------------------------------------------------------------------------------- /frontend/src/app/components/about/about.component.scss: -------------------------------------------------------------------------------- 1 | [mat-button] { 2 | width: 100%; 3 | } 4 | 5 | li { 6 | margin: 5px auto; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/components/howto/howto.component.scss: -------------------------------------------------------------------------------- 1 | [mat-button] { 2 | width: 100%; 3 | } 4 | 5 | li { 6 | margin: 5px auto; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/containers/main/main.component.scss: -------------------------------------------------------------------------------- 1 | mat-card { 2 | margin: 0 auto; 3 | max-width: 90%; 4 | width: 500px; 5 | 6 | } 7 | -------------------------------------------------------------------------------- /scripts/logsof.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | LOGFILE=$(docker inspect --format='{{.LogPath}}' $1) 4 | APP=${2:-vim} 5 | $APP $LOGFILE 6 | -------------------------------------------------------------------------------- /frontend/src/app/components/login/login.component.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | 5 | [mat-button] { 6 | width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/components/signup/signup.component.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | 5 | [mat-button] { 6 | width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3000/", 4 | "pathRewrite": { 5 | "^/api" : "" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm 3 | .#* 4 | npm-debug.log 5 | .tern-port 6 | puppy-love 7 | vendor 8 | views/build 9 | .mail.env 10 | .pass.env 11 | /res 12 | -------------------------------------------------------------------------------- /frontend/src/app/components/countdown/countdown.component.scss: -------------------------------------------------------------------------------- 1 | [mat-button] { 2 | width: 100%; 3 | } 4 | 5 | .mat-display-1 { 6 | text-align: center; 7 | } 8 | -------------------------------------------------------------------------------- /scripts/mongodump/puppy/heart.metadata.json: -------------------------------------------------------------------------------- 1 | {"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"puppy.heart"}],"uuid":"da3618e0c81640cea36b871c96496718"} -------------------------------------------------------------------------------- /scripts/mongodump/puppy/user.metadata.json: -------------------------------------------------------------------------------- 1 | {"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"puppy.user"}],"uuid":"e42ccd5dc0274dcc811d2f1c324271eb"} -------------------------------------------------------------------------------- /scripts/mongodump/puppy/declare.metadata.json: -------------------------------------------------------------------------------- 1 | {"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"puppy.declare"}],"uuid":"4643a53cc4fd43c999e25f584e81df08"} -------------------------------------------------------------------------------- /service/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # Set nginx base image 2 | FROM nginx 3 | 4 | # Copy custom configuration file from the current directory 5 | COPY nginx.prod.conf /etc/nginx/nginx.conf 6 | -------------------------------------------------------------------------------- /frontend/src/app/containers/main/main.component.html: -------------------------------------------------------------------------------- 1 | 2 | Puppy Love 3 | 4 | 5 | -------------------------------------------------------------------------------- /scripts/smallcompute.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | read -p "Roll: " id 4 | read -p "Required gender: " req 5 | 6 | http -v --timeout=24000 "localhost:3000/admin/compute/preparesmall/${id}/${req}" "$CADMIN" 7 | -------------------------------------------------------------------------------- /.backend.env: -------------------------------------------------------------------------------- 1 | SIGNUP_PORT_3001_TCP_PORT=3001 2 | SIGNUP_PORT_3001_TCP_ADDR=signup 3 | 4 | MONGO_PORT_27017_TCP_PORT=27017 5 | MONGO_PORT_27017_TCP_ADDR=mongo 6 | 7 | TABLE=declare 8 | 9 | GIN_MODE=release 10 | -------------------------------------------------------------------------------- /frontend/src/app/containers/how-it-works/how-it-works.component.html: -------------------------------------------------------------------------------- 1 | Puppy Love 2 | 3 | Back 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/app/containers/login/login.container.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /frontend/src/app/containers/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 | 6 | `, 7 | styles: [ '' ] 8 | }) 9 | export class HomeComponent { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/containers/main/main.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './main.component.html', 5 | styleUrls: [ './main.component.scss' ] 6 | }) 7 | export class MainComponent { 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/containers/about/about.container.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './about.container.html', 5 | styleUrls: [ './about.container.scss' ] 6 | }) 7 | export class AboutComponent { 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/containers/howto/howto.container.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './howto.container.html', 5 | styleUrls: [ './howto.container.scss' ] 6 | }) 7 | export class HowToComponent { 8 | } 9 | -------------------------------------------------------------------------------- /frontend/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('puppy-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/containers/how-it-works/how-it-works.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './how-it-works.component.html', 5 | styleUrls: [ './how-it-works.component.scss' ] 6 | }) 7 | export class HowItWorksComponent { 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/app/containers/signup/signup.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /frontend/src/app/components/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'puppy-about', 5 | templateUrl: './about.component.html', 6 | styleUrls: [ './about.component.scss' ] 7 | }) 8 | export class AboutComponent { 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/app/components/howto/howto.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'puppy-howto', 5 | templateUrl: './howto.component.html', 6 | styleUrls: [ './howto.component.scss' ] 7 | }) 8 | export class HowToComponent { 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | 3 | events { worker_connections 1024; } 4 | 5 | http { 6 | server { 7 | include /etc/nginx/mime.types; 8 | listen 80; 9 | location / { 10 | index index.html; 11 | root /src/dist; 12 | try_files $uri /index.html; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /scripts/newuser.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | read -p "Roll: " roll 4 | read -p "Name: " name 5 | read -p "Gender (0/1): " gender 6 | read -p "Image: " image 7 | read -p "Email: " email 8 | 9 | http -v 'pclub.cse.iitk.ac.in/api/admin/user/new' roll="$roll" name="$name" email="$email" gender="$gender" image="$image" passHash="aaaa" "$CADMIN" 10 | -------------------------------------------------------------------------------- /frontend/src/app/components/student/student.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | {{ student.name }}
6 | {{ student._id }}
7 |
8 |
9 | -------------------------------------------------------------------------------- /frontend/src/app/components/countdown/countdown.component.html: -------------------------------------------------------------------------------- 1 | 2 |
Time until portal is active:
3 |
{{ current }}
4 | How It Works 5 | About 6 | How To Use 7 |
8 | -------------------------------------------------------------------------------- /frontend/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('frontend App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to puppy!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "test.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /scripts/login.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Username: ${1}" 4 | VAL=$(http 'pclub.cse.iitk.ac.in/api/session/login' username="$1" password="$2" --header | tail -n 2 | head -n1) 5 | echo "Cookie is ${VAL}" 6 | 7 | if [ "$1" = "admin" ]; 8 | then 9 | echo "Logging in as admin" 10 | export CADMIN="Cookie:${VAL:12}" 11 | else 12 | echo "Logging in as ${1}" 13 | export COOKIE="Cookie:${VAL:12}" 14 | fi; 15 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | p { 2 | font-size: 35%; 3 | margin-top: 0px; 4 | margin-bottom: 0px; 5 | padding-top: 0px; 6 | padding-bottom: 0px; 7 | } 8 | 9 | h4 { 10 | margin-bottom: 0px; 11 | padding-bottom: 0px; 12 | } 13 | 14 | img { 15 | margin-top: 0px; 16 | margin-bottom: 0px; 17 | padding-top: 0px; 18 | padding-bottom: 0px; 19 | } 20 | 21 | h1 { 22 | margin-bottom: 20px; 23 | padding-bottom: 0px; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | 4 | @Component({ 5 | selector: 'puppy-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'] 8 | }) 9 | export class AppComponent implements OnInit { 10 | 11 | constructor( 12 | public snackBar: MatSnackBar, 13 | ) {} 14 | 15 | ngOnInit() { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'hammerjs'; 2 | 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | 6 | import { AppModule } from './app/app.module'; 7 | import { environment } from './environments/environment'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule) 14 | .catch(err => console.log(err)); 15 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # pclubiitk/puppy-love:frontend 2 | FROM node:8-alpine as builder 3 | 4 | # Cached layer for node_modules 5 | COPY package.json /tmp/package.json 6 | COPY yarn.lock /tmp/yarn.lock 7 | RUN cd /tmp && yarn install 8 | RUN mkdir -p /src && cp -a /tmp/node_modules /src/ 9 | 10 | WORKDIR /src 11 | COPY . . 12 | RUN yarn build 13 | 14 | FROM nginx 15 | 16 | RUN mkdir -p /src/dist 17 | COPY --from=builder /src/dist /src/dist 18 | COPY nginx.conf /etc/nginx/nginx.conf 19 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | Puppy Love @iitk! 5 |

Programming Club IIT Kanpur

6 |

in association with

7 | In association with techkriti 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /service/results/mongo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | ) 6 | 7 | type PuppyDb struct { 8 | S *mgo.Session 9 | } 10 | 11 | func MongoConnect() (PuppyDb, error) { 12 | S, err := mgo.Dial(CfgMgoUrl) 13 | return PuppyDb{S}, err 14 | } 15 | 16 | func (db PuppyDb) GetById(table string, id string) *mgo.Query { 17 | return db.S.DB("puppy").C(table).FindId(id) 18 | } 19 | 20 | func (db PuppyDb) GetCollection(table string) *mgo.Collection { 21 | return db.S.DB("puppy").C(table) 22 | } 23 | -------------------------------------------------------------------------------- /service/signup/mongo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | ) 6 | 7 | type PuppyDb struct { 8 | S *mgo.Session 9 | } 10 | 11 | func MongoConnect() (PuppyDb, error) { 12 | S, err := mgo.Dial(CfgMgoUrl) 13 | return PuppyDb{S}, err 14 | } 15 | 16 | func (db PuppyDb) GetById(table string, id string) *mgo.Query { 17 | return db.S.DB("puppy").C(table).FindId(id) 18 | } 19 | 20 | func (db PuppyDb) GetCollection(table string) *mgo.Collection { 21 | return db.S.DB("puppy").C(table) 22 | } 23 | -------------------------------------------------------------------------------- /models/hearts.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ( 4 | Heart struct { 5 | Id string `json:"roll" bson:"roll"` 6 | Gender string `json:"gender" bson:"gender"` 7 | Time uint64 `json:"time" bson:"time"` 8 | Value string `json:"v" bson:"v"` 9 | Data string `json:"data" bson:"data"` 10 | } 11 | 12 | GotHeart struct { 13 | Value string `json:"v" bson:"v"` 14 | Data string `json:"data" bson:"data"` 15 | GenderOfSender string `json:"genderOfSender" bson:"genderOfSender"` 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /frontend/src/app/guards/logged-in.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate } from '@angular/router'; 3 | 4 | import { MainService } from '../services/main.service'; 5 | 6 | @Injectable() 7 | export class LoggedInGuard implements CanActivate { 8 | constructor(private router: Router, 9 | private main: MainService) {} 10 | 11 | canActivate() { 12 | if (!this.main.loggedIn) { 13 | this.router.navigate([ '/login' ]); 14 | } 15 | return this.main.loggedIn; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/app/guards/logged-out.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate } from '@angular/router'; 3 | 4 | import { MainService } from '../services/main.service'; 5 | 6 | @Injectable() 7 | export class LoggedOutGuard implements CanActivate { 8 | constructor(private router: Router, 9 | private main: MainService) {} 10 | 11 | canActivate() { 12 | if (this.main.loggedIn) { 13 | this.router.navigate([ '/home' ]); 14 | } 15 | return !this.main.loggedIn; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /db/mongo.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/pclubiitk/puppy-love/config" 5 | 6 | "gopkg.in/mgo.v2" 7 | ) 8 | 9 | type PuppyDb struct { 10 | S *mgo.Session 11 | } 12 | 13 | func MongoConnect() (PuppyDb, error) { 14 | S, err := mgo.Dial(config.CfgMgoUrl) 15 | return PuppyDb{S}, err 16 | } 17 | 18 | func (db PuppyDb) GetById(table string, id string) *mgo.Query { 19 | return db.S.DB("puppy").C(table).FindId(id) 20 | } 21 | 22 | func (db PuppyDb) GetCollection(table string) *mgo.Collection { 23 | return db.S.DB("puppy").C(table) 24 | } 25 | -------------------------------------------------------------------------------- /service/results/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "v2" 6 | name = "gopkg.in/mgo.v2" 7 | packages = [".","bson","internal/json","internal/sasl","internal/scram"] 8 | revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655" 9 | 10 | [solve-meta] 11 | analyzer-name = "dep" 12 | analyzer-version = 1 13 | inputs-digest = "5bc236996eb0c3dd772accbe0b5122e79b1ed74e50984babc0664a2ad68ad18f" 14 | solver-name = "gps-cdcl" 15 | solver-version = 1 16 | -------------------------------------------------------------------------------- /service/signup/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "v2" 6 | name = "gopkg.in/mgo.v2" 7 | packages = [".","bson","internal/json","internal/sasl","internal/scram"] 8 | revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655" 9 | 10 | [solve-meta] 11 | analyzer-name = "dep" 12 | analyzer-version = 1 13 | inputs-digest = "5bc236996eb0c3dd772accbe0b5122e79b1ed74e50984babc0664a2ad68ad18f" 14 | solver-name = "gps-cdcl" 15 | solver-version = 1 16 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Puppy Love 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /service/results/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | var CfgMgoUrl = "mongodb://0.0.0.0:27017/puppy" 8 | var CfgTable = "declare" 9 | 10 | func CfgInit() { 11 | var port string 12 | var addr string 13 | 14 | // Mongo ENV variables 15 | port = os.Getenv("MONGO_PORT_27017_TCP_PORT") 16 | addr = os.Getenv("MONGO_PORT_27017_TCP_ADDR") 17 | if port != "" && addr != "" { 18 | CfgMgoUrl = "mongodb://" + addr + ":" + port + "/puppy" 19 | } 20 | 21 | var kk = os.Getenv("TABLE") 22 | if kk != "" { 23 | CfgTable = kk 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "assetGroups": [{ 4 | "name": "app", 5 | "installMode": "prefetch", 6 | "resources": { 7 | "files": [ 8 | "/favicon.ico", 9 | "/index.html" 10 | ], 11 | "versionedFiles": [ 12 | "/*.bundle.css", 13 | "/*.bundle.js", 14 | "/*.chunk.js" 15 | ] 16 | } 17 | }, { 18 | "name": "assets", 19 | "installMode": "lazy", 20 | "updateMode": "prefetch", 21 | "resources": { 22 | "files": [ 23 | "/assets/**" 24 | ] 25 | } 26 | }] 27 | } -------------------------------------------------------------------------------- /utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/pclubiitk/puppy-love/config" 10 | ) 11 | 12 | func SignupRequest(id string) error { 13 | response, err := http.Get(config.SignupUrl + "/" + id) 14 | if err != nil { 15 | log.Println("Signup service is faulty") 16 | log.Println(err) 17 | return err 18 | } else { 19 | defer response.Body.Close() 20 | _, err := io.Copy(os.Stdout, response.Body) 21 | if err != nil { 22 | log.Println("Signup service is faulty") 23 | log.Println(err) 24 | } 25 | return err 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/app/components/student/student.component.css: -------------------------------------------------------------------------------- 1 | .img { 2 | background-size: cover; 3 | flex-grow: 1; 4 | min-width: 140px; 5 | width: 20%; 6 | } 7 | 8 | .info { 9 | flex-grow: 3; 10 | padding: 2vmin; 11 | width: 80%; 12 | } 13 | 14 | .name { 15 | font-weight: 700; 16 | } 17 | 18 | @media only screen and (max-width: 600px) { 19 | .container { 20 | width: 80vw !important; 21 | } 22 | } 23 | 24 | .container { 25 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 26 | cursor: pointer; 27 | height: 140px; 28 | margin: 2vmin; 29 | /* padding: 1vmin; */ 30 | width: 400px; 31 | } 32 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | func CheckForFields(c *gin.Context, required []string) bool { 11 | for _, key := range required { 12 | if c.Param(key) == "" { 13 | return false 14 | } 15 | } 16 | return true 17 | } 18 | 19 | func Randinit() { 20 | rand.Seed(time.Now().UnixNano()) 21 | } 22 | 23 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 24 | 25 | func RandStringRunes(n int) string { 26 | b := make([]rune, n) 27 | for i := range b { 28 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 29 | } 30 | return string(b) 31 | } 32 | -------------------------------------------------------------------------------- /service/results/Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "gopkg.in/mgo.v2" 26 | -------------------------------------------------------------------------------- /service/signup/Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "gopkg.in/mgo.v2" 26 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | 22 | 23 | [[constraint]] 24 | name = "github.com/gin-gonic/gin" 25 | version = "1.2.0" 26 | -------------------------------------------------------------------------------- /service/signup/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | RUN apk --no-cache add openssl wget git 4 | ENV GOPATH=/go 5 | RUN go get -u github.com/golang/dep/cmd/dep 6 | 7 | # copies the Gopkg.toml and Gopkg.lock to WORKDIR 8 | RUN mkdir -p /go/src/github.com/pclubiitk/puppy-love/service/signup 9 | WORKDIR /go/src/github.com/pclubiitk/puppy-love/service/signup 10 | COPY Gopkg.toml Gopkg.lock ./ 11 | 12 | RUN dep ensure -v -vendor-only 13 | 14 | COPY . . 15 | RUN go build 16 | 17 | FROM alpine 18 | RUN mkdir -p /go/bin 19 | COPY --from=builder /go/src/github.com/pclubiitk/puppy-love/service/signup/signup /go/bin 20 | 21 | RUN apk --no-cache add ca-certificates 22 | EXPOSE 3001 23 | ENTRYPOINT ["/go/bin/signup"] 24 | -------------------------------------------------------------------------------- /service/results/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | RUN apk --no-cache add openssl wget git 4 | ENV GOPATH=/go 5 | RUN go get -u github.com/golang/dep/cmd/dep 6 | 7 | # copies the Gopkg.toml and Gopkg.lock to WORKDIR 8 | RUN mkdir -p /go/src/github.com/pclubiitk/puppy-love/service/results 9 | WORKDIR /go/src/github.com/pclubiitk/puppy-love/service/results 10 | COPY Gopkg.toml Gopkg.lock ./ 11 | 12 | RUN dep ensure -v -vendor-only 13 | 14 | COPY . . 15 | RUN go build 16 | 17 | FROM alpine 18 | RUN mkdir -p /go/bin 19 | COPY --from=builder /go/src/github.com/pclubiitk/puppy-love/service/results/results /go/bin 20 | 21 | RUN apk --no-cache add ca-certificates 22 | EXPOSE 3001 23 | ENTRYPOINT ["/go/bin/results"] 24 | -------------------------------------------------------------------------------- /frontend/src/app/components/search/search.component.html: -------------------------------------------------------------------------------- 1 |
2 | search 3 | 4 | 5 | 6 | 9 |
10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | testem.log 35 | /typings 36 | 37 | # e2e 38 | /e2e/*.js 39 | /e2e/*.map 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | RUN apk --no-cache add openssl wget git 4 | ENV GOPATH=/go 5 | RUN wget -O /usr/local/bin/dep https://github.com/golang/dep/releases/download/v0.4.1/dep-$(go env GOOS)-$(go env GOHOSTARCH) && chmod +x /usr/local/bin/dep 6 | 7 | RUN mkdir -p /go/src/github.com/pclubiitk/puppy-love 8 | WORKDIR /go/src/github.com/pclubiitk/puppy-love 9 | 10 | COPY Gopkg.toml Gopkg.lock ./ 11 | # copies the Gopkg.toml and Gopkg.lock to WORKDIR 12 | 13 | RUN dep ensure -v -vendor-only 14 | 15 | COPY . . 16 | RUN go build 17 | 18 | FROM alpine 19 | RUN mkdir -p /go/bin 20 | COPY --from=builder /go/src/github.com/pclubiitk/puppy-love/puppy-love /go/bin 21 | 22 | EXPOSE 3000 23 | ENTRYPOINT ["/go/bin/puppy-love"] 24 | 25 | -------------------------------------------------------------------------------- /frontend/src/app/containers/index.ts: -------------------------------------------------------------------------------- 1 | import { AboutComponent } from './about'; 2 | import { HowItWorksComponent } from './how-it-works'; 3 | import { HomeComponent } from './home'; 4 | import { HowToComponent } from './howto'; 5 | import { LoginComponent } from './login'; 6 | import { MainComponent } from './main'; 7 | import { SignupComponent } from './signup'; 8 | 9 | export const containers = [ 10 | AboutComponent, 11 | HomeComponent, 12 | HowItWorksComponent, 13 | HowToComponent, 14 | LoginComponent, 15 | MainComponent, 16 | SignupComponent, 17 | ]; 18 | 19 | export * from './about'; 20 | export * from './home'; 21 | export * from './how-it-works'; 22 | export * from './howto'; 23 | export * from './login'; 24 | export * from './main'; 25 | export * from './signup'; 26 | -------------------------------------------------------------------------------- /frontend/src/app/components/home/home.component.scss: -------------------------------------------------------------------------------- 1 | [mat-button] { 2 | width: 100%; 3 | } 4 | 5 | mat-form-field { 6 | width: 100%; 7 | } 8 | 9 | mat-card-title { 10 | text-align: center; 11 | } 12 | 13 | .profile { 14 | width: 20vw; 15 | } 16 | 17 | .profile-pic { 18 | width: 20vw; 19 | height: 20vw; 20 | background-size: cover; 21 | padding: 24px; 22 | flex-grow: 1; 23 | } 24 | 25 | .main { 26 | width: 66.67vw; 27 | } 28 | 29 | .smallonly { 30 | display: none; 31 | } 32 | 33 | .lovely-heart { 34 | max-width: 50px; 35 | } 36 | 37 | @media screen and (max-width: 640px) { 38 | .smallonly { 39 | display: block; 40 | } 41 | .main { 42 | display: block; 43 | width: 90vw; 44 | } 45 | .bigonly { 46 | display: none; 47 | } 48 | .textonly-greeter { 49 | margin: 5px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/src/app/components/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'puppy-login', 6 | templateUrl: './login.component.html', 7 | styleUrls: [ './login.component.scss' ] 8 | }) 9 | export class LoginComponent { 10 | 11 | loginForm: FormGroup; 12 | 13 | @Output() 14 | private login = new EventEmitter<{roll: string, password: string}>(); 15 | 16 | constructor(private fb: FormBuilder) { 17 | // Create Form 18 | this.loginForm = this.fb.group({ 19 | roll: ['', Validators.required], 20 | password: ['', Validators.required], 21 | }); 22 | } 23 | 24 | get loginInfo(): {roll: string, password: string} { 25 | return this.loginForm.value; 26 | } 27 | 28 | onSubmit() { 29 | this.login.emit(this.loginInfo); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/components/index.ts: -------------------------------------------------------------------------------- 1 | import { AboutComponent } from './about'; 2 | import { CountdownComponent } from './countdown'; 3 | import { HomeComponent } from './home'; 4 | import { HowToComponent } from './howto'; 5 | import { LoginComponent } from './login'; 6 | import { ResultsComponent } from './results'; 7 | import { SearchComponent } from './search'; 8 | import { SignupComponent } from './signup'; 9 | import { StudentComponent } from './student'; 10 | 11 | export const components = [ 12 | AboutComponent, 13 | CountdownComponent, 14 | HomeComponent, 15 | HowToComponent, 16 | LoginComponent, 17 | ResultsComponent, 18 | SearchComponent, 19 | SignupComponent, 20 | StudentComponent, 21 | ]; 22 | 23 | export * from './about'; 24 | export * from './countdown'; 25 | export * from './home'; 26 | export * from './howto'; 27 | export * from './login'; 28 | export * from './results'; 29 | export * from './search'; 30 | export * from './signup'; 31 | export * from './student'; 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mongo: 5 | image: mongo 6 | networks: 7 | - mynet 8 | 9 | results: 10 | image: pclub/puppy-love:results 11 | env_file: 12 | - .backend.env 13 | links: 14 | - mongo 15 | networks: 16 | - mynet 17 | 18 | signup: 19 | image: pclub/puppy-love:signup 20 | env_file: 21 | - .backend.env 22 | - .mail.env 23 | ports: 24 | - "3001" 25 | links: 26 | - mongo 27 | networks: 28 | - mynet 29 | 30 | backend: 31 | image: pclub/puppy-love:backend 32 | env_file: 33 | - .backend.env 34 | - .pass.env 35 | ports: 36 | - "34770:3000" 37 | links: 38 | - mongo 39 | - signup 40 | networks: 41 | - mynet 42 | 43 | frontend: 44 | image: pclub/puppy-love:frontend 45 | ports: 46 | - "34771:80" 47 | 48 | networks: 49 | mynet: 50 | -------------------------------------------------------------------------------- /frontend/src/app/components/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | Login 3 | 4 | 5 |
6 | 7 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | Create Your Account 23 | About 24 | How To Use 25 |
26 |
27 | 28 |
29 | Note that you need to create an account before you can log in. 30 |
31 |
32 | -------------------------------------------------------------------------------- /frontend/src/app/components/student/student.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { DomSanitizer } from '@angular/platform-browser'; 3 | 4 | function ImageURL(rollnum: string, userid: string) { 5 | const iitkhome = `http://home.iitk.ac.in/~${ userid }/dp`; 6 | const oaimage = `https://oa.cc.iitk.ac.in/Oa/Jsp/Photo/${ rollnum }_0.jpg`; 7 | return `url("${ iitkhome }"), url("${ oaimage }")`; 8 | } 9 | 10 | @Component({ 11 | selector: 'puppy-student', 12 | templateUrl: './student.component.html', 13 | styleUrls: ['./student.component.css'], 14 | }) 15 | export class StudentComponent { 16 | 17 | @Input() 18 | student: any; 19 | @Output() 20 | select = new EventEmitter(); 21 | 22 | constructor(private sanitizer: DomSanitizer) {} 23 | 24 | get url() { 25 | return this.sanitizer.bypassSecurityTrustStyle(ImageURL(this.student._id, this.student.email)); 26 | } 27 | 28 | selectStudent() { 29 | this.select.emit(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/containers/login/login.container.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { MatSnackBar } from '@angular/material/snack-bar'; 4 | import 'rxjs/add/operator/finally'; 5 | 6 | import { MainService } from '../../services/main.service'; 7 | 8 | @Component({ 9 | templateUrl: './login.container.html', 10 | styleUrls: [ './login.container.scss' ] 11 | }) 12 | export class LoginComponent { 13 | 14 | loading = false; 15 | 16 | constructor(private main: MainService, 17 | private router: Router, 18 | private snackBar: MatSnackBar) {} 19 | 20 | onLogin(login: { roll: string, password: string }) { 21 | this.loading = true; 22 | this.main.login(login.roll, login.password) 23 | .finally(() => this.loading = false) 24 | .subscribe( 25 | () => this.router.navigate([ 'home' ]), 26 | (err) => this.snackBar.open(err, '', { 27 | duration: 3000 28 | })); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/pclubiitk/puppy-love/config" 8 | "github.com/pclubiitk/puppy-love/db" 9 | "github.com/pclubiitk/puppy-love/router" 10 | "github.com/pclubiitk/puppy-love/utils" 11 | 12 | "github.com/gin-contrib/sessions" 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | func executeFirst(c *gin.Context) { 17 | // fmt.Println(string(ctx.Path()[:])) 18 | // ctx.Next() 19 | } 20 | 21 | func main() { 22 | config.CfgInit() 23 | 24 | mongoDb, error := db.MongoConnect() 25 | if error != nil { 26 | fmt.Print("[Error] Could not connect to MongoDB") 27 | fmt.Print("[Error] " + config.CfgMgoUrl) 28 | fmt.Print(os.Environ()) 29 | os.Exit(1) 30 | } 31 | 32 | utils.Randinit() 33 | 34 | // set up session db 35 | store := sessions.NewCookieStore([]byte(config.CfgAdminPass)) 36 | 37 | // iris.Config.Gzip = true 38 | r := gin.Default() 39 | r.Use(sessions.Sessions("mysession", store)) 40 | router.PuppyRoute(r, mongoDb) 41 | r.Run(config.CfgAddr) 42 | } 43 | -------------------------------------------------------------------------------- /service/signup/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | var EmailUser = os.Getenv("EMAIL_USER") 9 | var EmailPass = os.Getenv("EMAIL_PASS") 10 | var EmailHost = os.Getenv("EMAIL_HOST") 11 | var EmailPort = os.Getenv("EMAIL_PORT") 12 | 13 | var CfgAddr = ":3000" 14 | 15 | var CfgMgoUrl = "mongodb://0.0.0.0:27017/puppy" 16 | 17 | func CfgInit() { 18 | var port string 19 | var addr string 20 | 21 | // Email ENV variables 22 | if EmailUser == "" || EmailPass == "" { 23 | log.Println("WARNING: Email variables are not in scope") 24 | } 25 | 26 | if EmailHost == "" { 27 | log.Println("No email host") 28 | EmailHost = "smtp.gmail.com" 29 | } 30 | 31 | if EmailPort == "" { 32 | log.Println("No email port") 33 | EmailHost = "587" 34 | } 35 | 36 | // Mongo ENV variables 37 | port = os.Getenv("MONGO_PORT_27017_TCP_PORT") 38 | addr = os.Getenv("MONGO_PORT_27017_TCP_ADDR") 39 | if port != "" && addr != "" { 40 | CfgMgoUrl = "mongodb://" + addr + ":" + port + "/puppy" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scripts/script.js: -------------------------------------------------------------------------------- 1 | const MongoClient = require('mongodb').MongoClient; 2 | const url = 'mongodb://localhost:27017/'; 3 | const generatePassword = require('password-generator'); 4 | 5 | const students = require('./students.json'); 6 | 7 | MongoClient.connect(url, function(err, db) { 8 | if (err) throw err; 9 | console.log('Database created!'); 10 | const dbo = db.db('puppy'); 11 | const results = students.map((s) => { 12 | return { 13 | _id: s.i, 14 | name: s.n, 15 | email: s.u, 16 | image: '', 17 | gender: s.g === 'M' ? '1' : '0', 18 | passHash: 'aaaa', 19 | privKey: '', 20 | pubKey: '', 21 | authCode: generatePassword(15), 22 | data: '', 23 | submitted: false, 24 | matches: '', 25 | voted: 0, 26 | dirty: true, 27 | savepass: '' 28 | }; 29 | }); 30 | dbo.collection('user').insertMany(results , function(err, res) { 31 | if (err) throw err; 32 | console.log("Number of documents inserted: " + res.insertedCount); 33 | db.close(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Programming Club IIT Kanpur 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /models/declare.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | ) 7 | 8 | type ( 9 | Declare struct { 10 | Id string `json:"_id" bson:"_id"` 11 | Token0 string `json:"t0" bson:"t0"` 12 | Token1 string `json:"t1" bson:"t1"` 13 | Token2 string `json:"t2" bson:"t2"` 14 | Token3 string `json:"t3" bson:"t3"` 15 | } 16 | PairUpsert struct { 17 | Selector bson.M 18 | Change bson.M 19 | } 20 | ) 21 | 22 | // Create table update object for token table 23 | func UpsertDeclareTable(d *Declare) mgo.Change { 24 | return mgo.Change{ 25 | Update: bson.M{"$set": bson.M{ 26 | "t0": d.Token0, 27 | "t1": d.Token1, 28 | "t2": d.Token2, 29 | "t3": d.Token3, 30 | }}, 31 | ReturnNew: true, 32 | } 33 | } 34 | 35 | func NewDeclareTable(id string) PairUpsert { 36 | _selector := bson.M{"_id": id} 37 | 38 | return PairUpsert{ 39 | Selector: _selector, 40 | Change: bson.M{"$setOnInsert": bson.M{ 41 | "_id": id, 42 | "t0": "", 43 | "t1": "", 44 | "t2": "", 45 | "t3": "", 46 | }}, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.6.5. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /frontend/src/app/components/results/results.component.scss: -------------------------------------------------------------------------------- 1 | [mat-button] { 2 | width: 100%; 3 | } 4 | 5 | mat-form-field { 6 | width: 100%; 7 | } 8 | 9 | mat-card-title { 10 | text-align: center; 11 | } 12 | 13 | .profile { 14 | width: 20vw; 15 | } 16 | 17 | .profile-pic { 18 | width: 20vw; 19 | height: 20vw; 20 | background-size: cover; 21 | padding: 24px; 22 | flex-grow: 1; 23 | } 24 | 25 | .main { 26 | margin-bottom: 30px; 27 | width: 66.67vw; 28 | } 29 | 30 | .smallonly { 31 | display: none; 32 | } 33 | 34 | .lovely-heart { 35 | max-width: 50px; 36 | } 37 | 38 | .chart { 39 | border: 1px solid black; 40 | width: 18vw; 41 | height: 18vw; 42 | padding: 10px; 43 | margin: 10px; 44 | 45 | h2 { 46 | margin: 0; 47 | text-align: center; 48 | } 49 | } 50 | 51 | @media screen and (max-width: 640px) { 52 | .smallonly { 53 | display: block; 54 | } 55 | .main { 56 | display: block; 57 | width: 80vw; 58 | } 59 | .bigonly { 60 | display: none; 61 | } 62 | .textonly-greeter { 63 | margin: 5px; 64 | } 65 | .chart { 66 | width: 63vw; 67 | height: 63vw; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /puppy.nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name dev.puppy.pclub.in; 4 | 5 | location /api { 6 | # proxy_pass http://pclub.cse.iitk.ac.in; 7 | proxy_pass http://localhost:3000; 8 | proxy_set_header Connection ''; 9 | proxy_set_header Host 'pclub.cse.iitk.ac.in'; 10 | proxy_set_header Referrer 'pclub.cse.iitk.ac.in'; 11 | add_header 'Access-Control-Allow-Origin' $host; 12 | proxy_http_version 1.1; 13 | chunked_transfer_encoding off; 14 | proxy_connect_timeout 5m; 15 | proxy_read_timeout 5m; 16 | } 17 | 18 | location /Oa { 19 | proxy_pass http://oa.cc.iitk.ac.in:80; 20 | proxy_set_header Connection ''; 21 | add_header 'Access-Control-Allow-Origin' $host; 22 | proxy_http_version 1.1; 23 | chunked_transfer_encoding off; 24 | proxy_connect_timeout 5m; 25 | proxy_read_timeout 5m; 26 | } 27 | 28 | location / { 29 | proxy_pass http://localhost:8091/; 30 | proxy_http_version 1.1; 31 | proxy_read_timeout 86400; 32 | proxy_set_header Host 'pclub.cse.iitk.ac.in'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /service/results/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "gopkg.in/mgo.v2" 4 | import "gopkg.in/mgo.v2/bson" 5 | 6 | type ( 7 | // User represents the structure of our resource 8 | User struct { 9 | Id string `json:"_id" bson:"_id"` 10 | Name string `json:"name" bson:"name"` 11 | Email string `json:"email" bson:"email"` 12 | Gender string `json:"gender" bson:"gender"` 13 | Image string `json:"image" bson:"image"` 14 | Pass string `json:"passHash" bson:"passHash"` 15 | PrivK string `json:"privKey" bson:"privKey"` 16 | PubK string `json:"pubKey" bson:"pubKey"` 17 | AuthC string `json:"authCode" bson:"authCode"` 18 | Data string `json:"data" bson:"data"` 19 | Submit bool `json:"submitted" bson:"submitted"` 20 | Matches string `json:"matches" bson:"matches"` 21 | Vote int `json:"voted" bson:"voted"` 22 | Dirty bool `json:"dirty" bson:"dirty"` 23 | SPass string `json:"savepass" bson:"savepass"` 24 | } 25 | ) 26 | 27 | func (u User) MatchedUpdate(info string) mgo.Change { 28 | return mgo.Change{ 29 | Update: bson.M{"$set": bson.M{ 30 | "matches": info, 31 | }}, 32 | ReturnNew: true, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /service/nginx/nginx.prod.conf: -------------------------------------------------------------------------------- 1 | worker_processes 8; 2 | 3 | events { worker_connections 4096; } 4 | 5 | http { 6 | 7 | server { 8 | include /etc/nginx/mime.types; 9 | 10 | listen 80; 11 | 12 | location /api { 13 | proxy_pass http://backend:3000; 14 | rewrite /api/(.*) /$1 break; 15 | proxy_set_header Connection ''; 16 | add_header 'Access-Control-Allow-Origin' $host; 17 | proxy_http_version 1.1; 18 | chunked_transfer_encoding off; 19 | proxy_connect_timeout 5m; 20 | proxy_read_timeout 5m; 21 | } 22 | 23 | location ~* ^/Oa/Jsp/Photo/.+\.(jpg)$ { 24 | proxy_pass http://oa.cc.iitk.ac.in:80; 25 | proxy_set_header Connection ''; 26 | add_header 'Access-Control-Allow-Origin' $host; 27 | proxy_http_version 1.1; 28 | chunked_transfer_encoding off; 29 | proxy_connect_timeout 5m; 30 | proxy_read_timeout 5m; 31 | } 32 | 33 | location / { 34 | proxy_pass http://frontend; 35 | proxy_set_header Connection ''; 36 | add_header 'Access-Control-Allow-Origin' $host; 37 | proxy_http_version 1.1; 38 | chunked_transfer_encoding off; 39 | proxy_connect_timeout 5m; 40 | proxy_read_timeout 5m; 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /controllers/declare.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/pclubiitk/puppy-love/models" 8 | 9 | "github.com/gin-gonic/gin" 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | // @AUTH @Admin Create the entries in the declare table 14 | // ---------------------------------------------------- 15 | func DeclarePrepare(c *gin.Context) { 16 | id, err := SessionId(c) 17 | if err != nil || id != "admin" { 18 | c.AbortWithStatus(http.StatusForbidden) 19 | return 20 | } 21 | 22 | type typeIds struct { 23 | Id string `json:"_id" bson:"_id"` 24 | } 25 | 26 | var people []typeIds 27 | 28 | if err := Db.GetCollection("user"). 29 | Find(bson.M{"dirty": false}). 30 | All(&people); err != nil { 31 | c.AbortWithStatus(http.StatusInternalServerError) 32 | return 33 | } 34 | 35 | bulk := Db.GetCollection("declare").Bulk() 36 | for _, pe := range people { 37 | res := models.NewDeclareTable(pe.Id) 38 | bulk.Upsert(res.Selector, res.Change) 39 | } 40 | r, err := bulk.Run() 41 | 42 | if err != nil { 43 | c.AbortWithStatus(http.StatusInternalServerError) 44 | log.Println(err) 45 | return 46 | } 47 | c.JSON(http.StatusOK, r) 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { LoggedInGuard } from './guards/logged-in.guard'; 5 | import { LoggedOutGuard } from './guards/logged-out.guard'; 6 | 7 | import { 8 | AboutComponent, 9 | HowItWorksComponent, 10 | HowToComponent, 11 | LoginComponent, 12 | MainComponent, 13 | SignupComponent, 14 | HomeComponent, 15 | } from './containers'; 16 | 17 | const routes: Routes = [{ 18 | path: '', component: MainComponent, children: [ 19 | { path: '', pathMatch: 'full', redirectTo: 'login' }, 20 | { path: 'about', component: AboutComponent }, 21 | { path: 'how-it-works', component: HowItWorksComponent }, 22 | { path: 'howto', component: HowToComponent }, 23 | { path: 'login', component: LoginComponent, canActivate: [ LoggedOutGuard ] }, 24 | { path: 'signup', component: SignupComponent, canActivate: [ LoggedOutGuard ] }, 25 | ] 26 | }, { path: 'home', component: HomeComponent, canActivate: [ LoggedInGuard ] } 27 | ]; 28 | 29 | @NgModule({ 30 | imports: [RouterModule.forRoot(routes)], 31 | exports: [RouterModule] 32 | }) 33 | export class AppRoutingModule { } 34 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [ 8 | RouterTestingModule 9 | ], 10 | declarations: [ 11 | AppComponent 12 | ], 13 | }).compileComponents(); 14 | })); 15 | it('should create the app', async(() => { 16 | const fixture = TestBed.createComponent(AppComponent); 17 | const app = fixture.debugElement.componentInstance; 18 | expect(app).toBeTruthy(); 19 | })); 20 | it(`should have as title 'puppy'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('puppy'); 24 | })); 25 | it('should render title in a h1 tag', async(() => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to puppy!'); 30 | })); 31 | }); 32 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | var ( 8 | EmailUser = os.Getenv("EMAIL_USER") 9 | EmailPass = os.Getenv("EMAIL_PASS") 10 | CfgAdminPass = "passhash" 11 | CfgAddr = ":3000" 12 | CfgMgoUrl = "mongodb://0.0.0.0:27017/puppy" 13 | CfgRedisUrl = "0.0.0.0:6379" 14 | SignupUrl string = "http://0.0.0.0:3001" 15 | ) 16 | 17 | func CfgInit() { 18 | var port string 19 | var addr string 20 | 21 | // Signup URL 22 | port = os.Getenv("SIGNUP_PORT_3001_TCP_PORT") 23 | addr = os.Getenv("SIGNUP_PORT_3001_TCP_ADDR") 24 | if port != "" && addr != "" { 25 | SignupUrl = "http://" + addr + ":" + port 26 | } 27 | 28 | // Mongo ENV variables 29 | port = os.Getenv("MONGO_PORT_27017_TCP_PORT") 30 | addr = os.Getenv("MONGO_PORT_27017_TCP_ADDR") 31 | if port != "" && addr != "" { 32 | CfgMgoUrl = "mongodb://" + addr + ":" + port + "/puppy" 33 | } 34 | 35 | // Redis ENV variables 36 | port = os.Getenv("REDIS_PORT_6379_TCP_PORT") 37 | addr = os.Getenv("REDIS_PORT_6379_TCP_ADDR") 38 | if port != "" && addr != "" { 39 | CfgRedisUrl = addr + ":" + port 40 | } 41 | 42 | // Admin pass 43 | pass := os.Getenv("ADMIN_PASS") 44 | if pass != "" { 45 | CfgAdminPass = pass 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/app/components/about/about.component.html: -------------------------------------------------------------------------------- 1 | 2 | About 3 | 4 | 5 |

6 | Who said win-win situations do not exist? 7 |
8 | Puppy Love helps you meet your crush, anonymously. 9 |

10 |

11 | Express your interest, and both of you will be informed if the 12 | interest is mutual. Otherwise, no one will EVER 13 | know about your choice. No, not even the admin, i.e. us. 14 |

15 |

Features

16 | 22 |

Want to read the algorithm?

23 |

Puppy Love Alogorithm Report

24 |
25 | 26 | Back 27 | 28 | -------------------------------------------------------------------------------- /frontend/src/app/containers/signup/signup.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { MatSnackBar } from '@angular/material/snack-bar'; 4 | import 'rxjs/add/operator/finally'; 5 | 6 | import { MainService } from '../../services/main.service'; 7 | 8 | @Component({ 9 | templateUrl: './signup.component.html', 10 | styleUrls: [ './signup.component.scss' ] 11 | }) 12 | export class SignupComponent { 13 | 14 | loading = false; 15 | 16 | constructor(private main: MainService, 17 | private router: Router, 18 | private snackBar: MatSnackBar) {} 19 | 20 | signup(event: any) { 21 | this.loading = true; 22 | this.main.signup(event) 23 | .finally(() => this.loading = false) 24 | .subscribe( 25 | () => this.router.navigate([ 'login' ]), 26 | (err) => this.snackBar.open(err, '', { 27 | duration: 3000 28 | })); 29 | } 30 | 31 | mail(roll: string) { 32 | this.loading = true; 33 | this.main.mail(roll) 34 | .finally(() => this.loading = false) 35 | .subscribe( 36 | (msg) => { 37 | console.log(msg); 38 | this.snackBar.open(msg, '', { 39 | duration: 3000 40 | }) 41 | }); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /controllers/heart.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | 11 | "gopkg.in/mgo.v2/bson" 12 | ) 13 | 14 | func HeartGet(c *gin.Context) { 15 | id, err := SessionId(c) 16 | if err != nil || id != c.Param("you") { 17 | c.AbortWithStatus(http.StatusForbidden) 18 | return 19 | } 20 | 21 | // Last checked time 22 | ltime, err := strconv.ParseUint(c.Param("time"), 10, 64) 23 | if err != nil { 24 | c.String(http.StatusBadRequest, "Bad timestamp value") 25 | return 26 | } 27 | 28 | // Current time 29 | ctime := uint64(time.Now().UnixNano() / 1000000) 30 | 31 | // TODO: fix bindings to be consistent across dbs 32 | type AnonymVote struct { 33 | Value string `json:"v" bson:"v"` 34 | GenderOfSender string `json:"genderOfSender" bson:"gender"` 35 | } 36 | 37 | votes := new([]AnonymVote) 38 | 39 | // Fetch user 40 | if err := Db.GetCollection("heart"). 41 | Find(bson.M{"time": bson.M{"$gt": ltime, "$lte": ctime}}). 42 | All(votes); err != nil { 43 | c.AbortWithStatus(http.StatusNotFound) 44 | log.Print(err) 45 | return 46 | } 47 | 48 | if *votes == nil { 49 | *votes = []AnonymVote{} 50 | } 51 | 52 | c.JSON(http.StatusAccepted, bson.M{ 53 | "votes": *votes, 54 | "time": ctime, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /service/signup/signup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | ) 10 | 11 | type appHandler struct { 12 | Channel chan string 13 | } 14 | 15 | func (ctx appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 16 | ctx.Channel <- r.URL.Path[:] 17 | fmt.Fprintf(w, "Welcome, %s!", r.URL.Path[1:]) 18 | } 19 | 20 | func main() { 21 | CfgInit() 22 | mongoDb, error := MongoConnect() 23 | if error != nil { 24 | fmt.Print("[Error] Could not connect to MongoDB") 25 | os.Exit(1) 26 | } 27 | 28 | // Create channels for services 29 | signup_channel := make(chan string) 30 | mailer_channel := make(chan User) 31 | queue_channel := make(chan string) 32 | go SignupService(mongoDb, signup_channel, mailer_channel) 33 | go MailerService(mongoDb, mailer_channel) 34 | go QueueService(queue_channel, signup_channel) 35 | 36 | // For clean exit 37 | c := make(chan os.Signal, 1) 38 | signal.Notify(c, os.Interrupt) 39 | go func() { 40 | for sig := range c { 41 | log.Printf("Captured %v, exiting..", sig) 42 | close(signup_channel) 43 | close(mailer_channel) 44 | close(queue_channel) 45 | // TODO exit mongo and redis too 46 | os.Exit(0) 47 | } 48 | }() 49 | 50 | http.Handle("/", appHandler{queue_channel}) 51 | http.ListenAndServe(":3001", nil) 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/app/components/search/search.component.css: -------------------------------------------------------------------------------- 1 | .width-60 { 2 | background-color: #fff; 3 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 4 | margin: 0 auto; 5 | padding-top: 20px; 6 | width: 60%; 7 | } 8 | 9 | @media (max-width: 600px) { 10 | .width-60 { 11 | width: 90%; 12 | } 13 | } 14 | 15 | .search-options { 16 | margin: 15px; 17 | padding: 5px; 18 | } 19 | 20 | .mat-select-underline { 21 | background-color: #fff !important; 22 | color: #fff; 23 | } 24 | 25 | .text-area { 26 | background-color: #fff; 27 | } 28 | 29 | .search-grid { 30 | background-color: #eee; 31 | width: 100%; 32 | } 33 | 34 | .search-grid button { 35 | margin: 10px; 36 | } 37 | 38 | .search-icon { 39 | margin: 18px; 40 | } 41 | 42 | mat-spinner { 43 | margin: 10vh auto; 44 | } 45 | 46 | masonry { 47 | margin: 0 auto; 48 | width: 80%; 49 | } 50 | 51 | .fab { 52 | bottom: 2%; 53 | height: 2.8rem; 54 | position: fixed; 55 | right: 2%; 56 | text-align: center; 57 | width : 2.8rem; 58 | } 59 | 60 | .fab mat-icon { 61 | height: 2.8rem; 62 | width : 2.8rem; 63 | } 64 | 65 | .help { 66 | height: 2.8rem; 67 | position: fixed; 68 | right: 2%; 69 | top: 2%; 70 | width : 2.8rem; 71 | } 72 | 73 | .result-count { 74 | background-color: #eeeeee; 75 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 76 | margin: 10px; 77 | padding: 10px; 78 | } 79 | -------------------------------------------------------------------------------- /frontend/src/app/components/signup/signup.component.html: -------------------------------------------------------------------------------- 1 | 2 | Send verification mail 3 | 4 | 5 |
6 | 7 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | Signup 19 | 20 | 21 |
22 | 23 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 |
54 |
55 | 56 | -------------------------------------------------------------------------------- /frontend/src/app/components/countdown/countdown.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import * as moment from 'moment'; 4 | import { interval } from 'rxjs/observable/interval'; 5 | import { Subscription } from 'rxjs/Subscription'; 6 | 7 | @Component({ 8 | selector: 'puppy-countdown', 9 | templateUrl: './countdown.component.html', 10 | styleUrls: [ './countdown.component.scss' ] 11 | }) 12 | export class CountdownComponent implements OnInit { 13 | current: string; 14 | subs: Subscription; 15 | 16 | ngOnInit() { 17 | this.updateCurrent(); 18 | this.subs = interval(1000).subscribe(() => this.updateCurrent()); 19 | } 20 | 21 | ngOnDestroy() { 22 | if (this.subs) 23 | this.subs.unsubscribe(); 24 | } 25 | 26 | updateCurrent() { 27 | const eventdate = moment([2019, 1, 8]); 28 | const todaysdate = moment(); 29 | const totalSeconds = eventdate.diff(todaysdate, 'seconds'); 30 | const seconds = totalSeconds % 60; 31 | const totalminutes = Math.floor(totalSeconds / 60); 32 | const minutes = totalminutes % 60; 33 | const hours = Math.floor(totalminutes / 60); 34 | let hourString = "" + hours; 35 | let minuteString = "" + minutes; 36 | let secondsString = "" + seconds; 37 | if (hourString.length < 2) { 38 | hourString = "0" + hourString; 39 | } 40 | if (minuteString.length < 2) { 41 | minuteString = "0" + minuteString; 42 | } 43 | if (secondsString.length < 2) { 44 | secondsString = "0" + secondsString; 45 | } 46 | this.current = `${ hourString }:${ minuteString }:${ secondsString }`; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /frontend/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "frontend" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "puppy", 21 | "styles": [ 22 | "styles.scss", 23 | "../node_modules/@angular/material/prebuilt-themes/indigo-pink.css" 24 | ], 25 | "scripts": [ 26 | "sjcl.js" 27 | ], 28 | "environmentSource": "environments/environment.ts", 29 | "environments": { 30 | "dev": "environments/environment.ts", 31 | "prod": "environments/environment.prod.ts" 32 | }, 33 | "serviceWorker": true 34 | } 35 | ], 36 | "e2e": { 37 | "protractor": { 38 | "config": "./protractor.conf.js" 39 | } 40 | }, 41 | "lint": [ 42 | { 43 | "project": "src/tsconfig.app.json", 44 | "exclude": "**/node_modules/**" 45 | }, 46 | { 47 | "project": "src/tsconfig.spec.json", 48 | "exclude": "**/node_modules/**" 49 | }, 50 | { 51 | "project": "e2e/tsconfig.e2e.json", 52 | "exclude": "**/node_modules/**" 53 | } 54 | ], 55 | "test": { 56 | "karma": { 57 | "config": "./karma.conf.js" 58 | } 59 | }, 60 | "defaults": { 61 | "styleExt": "scss", 62 | "component": {} 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scripts/allusers.py: -------------------------------------------------------------------------------- 1 | import requests as r 2 | import sys 3 | 4 | with open(sys.argv[1]) as f: 5 | text = f.readlines() 6 | 7 | if len(sys.argv) >= 3: 8 | host = sys.argv[2] 9 | else: 10 | host = 'http://localhost:3000' 11 | 12 | url = host + '/admin/user/new' 13 | loginurl = host + '/session/login' 14 | logouturl = host + '/session/logout' 15 | 16 | ROLLNUM=0 17 | NAME=2 18 | EMAIL=7 19 | GENDER=9 20 | IMAGE=10 21 | 22 | s = r.Session() 23 | print("Logging in") 24 | logininfo = {} 25 | logininfo["username"] = "admin" 26 | logininfo["password"] = "passhash" 27 | resp = s.post(loginurl, json=logininfo) 28 | print(resp.status_code) 29 | print("Cookie: ", end='') 30 | print(s.cookies) 31 | 32 | if resp.status_code != 200: 33 | print("Could not login") 34 | exit() 35 | 36 | for person in text: 37 | data = list(map(lambda x: x.strip(), person.split(','))) 38 | 39 | payload = {} 40 | 41 | payload['roll'] = data[ROLLNUM] 42 | payload['name'] = data[NAME][1:-1] 43 | payload['email'] = data[EMAIL].split('@')[0] 44 | payload['image'] = data[IMAGE] 45 | if payload['email'] == "\"Not Available\"": 46 | continue 47 | 48 | if data[GENDER] == 'M': 49 | payload['gender'] = "1" 50 | elif data[GENDER] == 'F': 51 | payload['gender'] = "0" 52 | else: 53 | print("ERROR: finding gender of person {}".format(payload[roll])) 54 | continue 55 | 56 | print(payload) 57 | 58 | resp = s.post(url, json=payload) 59 | print("Code: " + str(resp.status_code)) 60 | if resp.status_code > 299: 61 | print(resp.text) 62 | 63 | print("Logging out... ", end='') 64 | print(s.get(logouturl).status_code) 65 | -------------------------------------------------------------------------------- /frontend/src/app/components/howto/howto.component.html: -------------------------------------------------------------------------------- 1 | 2 | How To Use 3 | 4 | 5 |

6 | Puppy Love lets you find mutual interests among people on campus. And it is simple to use! 7 |

8 |

9 | The platform is ours, but the love-algorithm is yours! Express your puppy love, and enjoy! 10 |

11 |

Steps

12 | 26 |
Good Luck!
27 |
28 | 29 | Back 30 | 31 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve --proxy proxy.conf.json", 8 | "build": "ng build --prod --build-optimizer", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^5.2.0", 16 | "@angular/cdk": "^5.1.0", 17 | "@angular/common": "^5.2.0", 18 | "@angular/compiler": "^5.2.0", 19 | "@angular/core": "^5.2.0", 20 | "@angular/flex-layout": "^2.0.0-beta.12", 21 | "@angular/forms": "^5.2.0", 22 | "@angular/http": "^5.2.0", 23 | "@angular/material": "^5.1.0", 24 | "@angular/platform-browser": "^5.2.0", 25 | "@angular/platform-browser-dynamic": "^5.2.0", 26 | "@angular/router": "^5.2.0", 27 | "@angular/service-worker": "^5.2.0", 28 | "@swimlane/ngx-charts": "^7.0.1", 29 | "core-js": "^2.4.1", 30 | "d3": "^4.13.0", 31 | "hammerjs": "^2.0.8", 32 | "moment": "^2.20.1", 33 | "rxjs": "^5.5.6", 34 | "sjcl": "^1.0.8", 35 | "zone.js": "^0.8.19" 36 | }, 37 | "devDependencies": { 38 | "@angular/cli": "1.6.7", 39 | "@angular/compiler-cli": "^5.2.0", 40 | "@angular/language-service": "^5.2.0", 41 | "@types/jasmine": "~2.8.3", 42 | "@types/jasminewd2": "~2.0.2", 43 | "@types/node": "~6.0.60", 44 | "codelyzer": "^4.0.1", 45 | "jasmine-core": "~2.8.0", 46 | "jasmine-spec-reporter": "~4.2.1", 47 | "karma": "~2.0.0", 48 | "karma-chrome-launcher": "~2.2.0", 49 | "karma-coverage-istanbul-reporter": "^1.2.1", 50 | "karma-jasmine": "~1.1.0", 51 | "karma-jasmine-html-reporter": "^0.2.2", 52 | "protractor": "~5.1.2", 53 | "ts-node": "~4.1.0", 54 | "tslint": "~5.9.1", 55 | "typescript": "~2.5.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/pclubiitk/puppy-love/controllers" 7 | "github.com/pclubiitk/puppy-love/db" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func PuppyRoute(r *gin.Engine, db db.PuppyDb) { 13 | 14 | r.GET("/", func(c *gin.Context) { 15 | c.JSON(http.StatusAccepted, "Hello from the other side!") 16 | }) 17 | 18 | controllers.Db = db 19 | 20 | r.GET("/stats", controllers.GetStats) 21 | 22 | // User administration 23 | users := r.Group("/users") 24 | { 25 | users.POST("/login/first", controllers.UserFirst) 26 | users.POST("/data/update/:you", controllers.UserUpdateData) 27 | users.POST("/data/submit/:you", controllers.UserSubmitTrue) 28 | users.POST("/image/update/:you", controllers.UserUpdateImage) 29 | users.POST("/pass/update/:you", controllers.UserSavePass) 30 | 31 | users.GET("/data/info", controllers.UserLoginGet) 32 | users.GET("/data/match/:you", controllers.MatchGet) 33 | users.GET("/get/:id", controllers.UserGet) 34 | users.GET("/mail/:id", controllers.UserMail) 35 | } 36 | 37 | // Listing users 38 | list := r.Group("/list") 39 | { 40 | list.GET("/all", controllers.ListAll) 41 | list.GET("/pubkey", controllers.PubkeyList) 42 | list.GET("/declare", controllers.DeclareList) 43 | } 44 | 45 | // Hearts 46 | hearts := r.Group("/hearts") 47 | { 48 | hearts.GET("/get/:time/:you", controllers.HeartGet) 49 | } 50 | 51 | // Session administration 52 | session := r.Group("/session") 53 | { 54 | session.POST("/login", controllers.SessionLogin) 55 | session.GET("/logout", controllers.SessionLogout) 56 | } 57 | 58 | // Admin 59 | admin := r.Group("/admin") 60 | { 61 | admin.GET("/declare/prepare", controllers.DeclarePrepare) 62 | admin.GET("/user/drop", controllers.UserDelete) 63 | admin.POST("/user/new", controllers.UserNew) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /service/signup/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gopkg.in/mgo.v2/bson" 5 | ) 6 | 7 | type ( 8 | // User represents the structure of our resource 9 | User struct { 10 | Id string `json:"_id" bson:"_id"` 11 | Name string `json:"name" bson:"name"` 12 | Email string `json:"email" bson:"email"` 13 | Gender string `json:"gender" bson:"gender"` 14 | Image string `json:"image" bson:"image"` 15 | Pass string `json:"passHash" bson:"passHash"` 16 | PrivK string `json:"privKey" bson:"privKey"` 17 | PubK string `json:"pubKey" bson:"pubKey"` 18 | AuthC string `json:"authCode" bson:"authCode"` 19 | Data string `json:"data" bson:"data"` 20 | Submit bool `json:"submitted" bson:"submitted"` 21 | Matches string `json:"matches" bson:"matches"` 22 | Vote int `json:"voted" bson:"voted"` 23 | Dirty bool `json:"dirty" bson:"dirty"` 24 | } 25 | ) 26 | 27 | type PairUpsert struct { 28 | Selector bson.M 29 | Change bson.M 30 | } 31 | 32 | func UpsertEntry(id1 string, id2 string) PairUpsert { 33 | itemId := id1 + "-" + id2 34 | p1 := id1 35 | p2 := id2 36 | if id1 > id2 { 37 | itemId = id2 + "-" + id1 38 | p1 = id2 39 | p2 = id1 40 | } 41 | 42 | _selector := bson.M{"_id": itemId} 43 | 44 | return PairUpsert{ 45 | Selector: _selector, 46 | Change: bson.M{"$setOnInsert": bson.M{ 47 | "_id": itemId, 48 | "p1": p1, 49 | "p2": p2, 50 | "t0": bson.M{}, 51 | "t1": bson.M{}, 52 | "r0": "", 53 | "r1": "", 54 | "v0": "", 55 | "v1": "", 56 | "match": false, 57 | }}, 58 | } 59 | } 60 | 61 | func NewDeclareTable(id string) PairUpsert { 62 | _selector := bson.M{"_id": id} 63 | 64 | return PairUpsert{ 65 | Selector: _selector, 66 | Change: bson.M{"$setOnInsert": bson.M{ 67 | "_id": id, 68 | "t0": "", 69 | "t1": "", 70 | "t2": "", 71 | "t3": "", 72 | }}, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /controllers/list.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/pclubiitk/puppy-love/models" 8 | 9 | "github.com/gin-gonic/gin" 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | // @AUTH Get user's basic information 14 | // --------------------------------------- 15 | type typeListAll struct { 16 | Id string `json:"_id" bson:"_id"` 17 | Name string `json:"name" bson:"name"` 18 | Email string `json:"email" bson:"email"` 19 | Image string `json:"image" bson:"image"` 20 | } 21 | 22 | func ListAll(c *gin.Context) { 23 | var results []typeListAll 24 | 25 | // Fetch user 26 | if err := Db.GetCollection("user"). 27 | Find(bson.M{}). 28 | All(&results); err != nil { 29 | 30 | c.AbortWithStatus(http.StatusNotFound) 31 | log.Print(err) 32 | return 33 | } 34 | 35 | c.JSON(http.StatusAccepted, results) 36 | } 37 | 38 | func PubkeyList(c *gin.Context) { 39 | var query [](struct { 40 | Id string `json:"_id" bson:"_id"` 41 | PK string `json:"pubKey" bson:"pubKey"` 42 | }) 43 | 44 | if err := Db.GetCollection("user"). 45 | Find(bson.M{"dirty": false}). 46 | All(&query); err != nil { 47 | 48 | c.AbortWithStatus(http.StatusNotFound) 49 | log.Print(err) 50 | return 51 | } 52 | 53 | c.JSON(http.StatusAccepted, query) 54 | } 55 | 56 | func DeclareList(c *gin.Context) { 57 | id, err := SessionId(c) 58 | if err != nil { 59 | c.AbortWithStatus(http.StatusForbidden) 60 | return 61 | } 62 | 63 | var resp models.Declare 64 | if err := Db.GetById("declare", id).One(&resp); err != nil { 65 | c.AbortWithStatus(http.StatusNotFound) 66 | log.Print(err) 67 | return 68 | } 69 | 70 | if resp.Token0 != "" { 71 | resp.Token0 = "1" 72 | } 73 | if resp.Token1 != "" { 74 | resp.Token1 = "1" 75 | } 76 | if resp.Token2 != "" { 77 | resp.Token2 = "1" 78 | } 79 | if resp.Token3 != "" { 80 | resp.Token3 = "1" 81 | } 82 | 83 | c.JSON(http.StatusOK, resp) 84 | } 85 | -------------------------------------------------------------------------------- /frontend/src/app/components/signup/signup.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; 3 | 4 | import { Crypto } from '../../crypto'; 5 | 6 | const passwordMatchValidator: ValidatorFn = (g: FormGroup) => { 7 | return g.get('password').value === g.get('password1').value ? null : { mismatch: true }; 8 | }; 9 | 10 | @Component({ 11 | selector: 'puppy-signup', 12 | templateUrl: './signup.component.html', 13 | styleUrls: [ './signup.component.scss' ] 14 | }) 15 | export class SignupComponent { 16 | 17 | mailForm: FormGroup; 18 | signupForm: FormGroup; 19 | 20 | @Output() 21 | private mail = new EventEmitter(); 22 | @Output() 23 | private signup = new EventEmitter(); 24 | 25 | constructor(private fb: FormBuilder) { 26 | // Create Form 27 | this.mailForm = this.fb.group({ 28 | roll: ['', Validators.required], 29 | }); 30 | this.signupForm = this.fb.group({ 31 | roll: ['', Validators.required], 32 | password: ['', Validators.required], 33 | password1: ['', Validators.required], 34 | authCode: ['', Validators.required], 35 | }, { validator: passwordMatchValidator }); 36 | } 37 | 38 | onMail() { 39 | this.mail.emit(this.mailForm.value.roll); 40 | } 41 | 42 | onSignup() { 43 | const { authCode, password, roll } = this.signupForm.value; 44 | 45 | const beginData = Crypto.fromJson({ 46 | choices: [] 47 | }); 48 | 49 | const crypto = new Crypto(password); 50 | // const crypto2 = new Crypto(ccpass); 51 | 52 | const passHash = Crypto.hash(Crypto.hash(Crypto.hash(password))); 53 | 54 | crypto.newKey(); 55 | 56 | // Store encrypted private key, public key, and encrypted empty data 57 | const body = { 58 | roll, 59 | passHash, 60 | authCode, 61 | privKey: crypto.encryptSym(crypto.serializePriv()), 62 | pubKey: crypto.serializePub(), 63 | // savePass: crypto2.encryptSym(password), 64 | data: crypto.encryptSym(beginData) 65 | }; 66 | 67 | this.signup.emit(body); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /controllers/session.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/pclubiitk/puppy-love/config" 10 | "github.com/pclubiitk/puppy-love/models" 11 | 12 | "github.com/gin-contrib/sessions" 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | type LoginInfo struct { 17 | Username string `json:"username" xml:"username" form:"username"` 18 | Passhash string `json:"password" xml:"password" form:"password"` 19 | } 20 | 21 | func SessionLogin(c *gin.Context) { 22 | session := sessions.Default(c) 23 | if session.Get("Status") != nil { 24 | session.Clear() 25 | session.Save() 26 | } 27 | 28 | u := new(LoginInfo) 29 | if err := c.BindJSON(u); err != nil { 30 | c.AbortWithStatus(http.StatusBadRequest) 31 | log.Println(err) 32 | return 33 | } 34 | 35 | // @TODO @IMPORTANT Move password to env variable 36 | if u.Username == "admin" { 37 | if u.Passhash == config.CfgAdminPass { 38 | session.Set("Status", "login") 39 | session.Set("id", u.Username) 40 | session.Save() 41 | c.String(http.StatusOK, 42 | fmt.Sprintf("Logged in: %s", u.Username)) 43 | } else { 44 | SessionLogout(c) 45 | c.String(http.StatusOK, "Invalid username or password") 46 | } 47 | return 48 | } 49 | 50 | user := models.User{} 51 | 52 | // Fetch user 53 | if err := Db.GetById("user", u.Username).One(&user); err != nil { 54 | SessionLogout(c) 55 | c.String(http.StatusNotFound, "Invalid user") 56 | log.Println("Invalid user: " + u.Username) 57 | return 58 | } 59 | 60 | // Check login 61 | if user.Pass == u.Passhash { 62 | session.Set("Status", "login") 63 | session.Set("id", u.Username) 64 | session.Save() 65 | c.JSON(http.StatusOK, gin.H{ 66 | "username": u.Username, 67 | }) 68 | } else { 69 | SessionLogout(c) 70 | c.AbortWithStatus(http.StatusForbidden) 71 | } 72 | } 73 | 74 | func SessionLogout(c *gin.Context) { 75 | sessions.Default(c).Clear() 76 | sessions.Default(c).Save() 77 | } 78 | 79 | func SessionId(c *gin.Context) (string, error) { 80 | id := sessions.Default(c).Get("id") 81 | if id != nil { 82 | return id.(string), nil 83 | } 84 | return "", errors.New("No such session") 85 | } 86 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | import { NgModule } from '@angular/core'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | 8 | import { NgxChartsModule } from '@swimlane/ngx-charts'; 9 | 10 | import { MatButtonModule } from '@angular/material/button'; 11 | import { MatCardModule } from '@angular/material/card'; 12 | import { MatDividerModule } from '@angular/material/divider'; 13 | import { MatChipsModule } from '@angular/material/chips'; 14 | import { MatFormFieldModule } from '@angular/material/form-field'; 15 | import { MatIconModule } from '@angular/material/icon'; 16 | import { MatInputModule } from '@angular/material/input'; 17 | import { MatListModule } from '@angular/material/list'; 18 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 19 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 20 | 21 | import { AppRoutingModule } from './app-routing.module'; 22 | 23 | import { AppComponent } from './app.component'; 24 | 25 | import { environment } from '../environments/environment'; 26 | import { components } from './components'; 27 | import { containers } from './containers'; 28 | import { MainService } from './services/main.service'; 29 | import { LoggedInGuard } from './guards/logged-in.guard'; 30 | import { LoggedOutGuard } from './guards/logged-out.guard'; 31 | 32 | @NgModule({ 33 | declarations: [ 34 | AppComponent, 35 | ...components, 36 | ...containers, 37 | ], 38 | imports: [ 39 | BrowserModule, 40 | BrowserAnimationsModule, 41 | HttpClientModule, 42 | ReactiveFormsModule, 43 | NgxChartsModule, 44 | 45 | FlexLayoutModule, 46 | MatButtonModule, 47 | MatCardModule, 48 | MatChipsModule, 49 | MatDividerModule, 50 | MatFormFieldModule, 51 | MatIconModule, 52 | MatInputModule, 53 | MatListModule, 54 | MatProgressSpinnerModule, 55 | MatSnackBarModule, 56 | 57 | AppRoutingModule, 58 | ], 59 | providers: [ LoggedInGuard, LoggedOutGuard, MainService ], 60 | bootstrap: [AppComponent] 61 | }) 62 | export class AppModule { } 63 | -------------------------------------------------------------------------------- /service/signup/routines.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/smtp" 6 | 7 | "gopkg.in/mgo.v2" 8 | "gopkg.in/mgo.v2/bson" 9 | ) 10 | 11 | func MarkNotDirtyAlt(u User) mgo.Change { 12 | return mgo.Change{ 13 | Update: bson.M{"$set": bson.M{ 14 | "dirty": false, 15 | }}, 16 | ReturnNew: true, 17 | } 18 | } 19 | 20 | func QueueService(listen_channel chan string, signup_channel chan string) { 21 | for request := range listen_channel { 22 | go func(request string) { 23 | signup_channel <- request[1:] 24 | }(request) 25 | } 26 | } 27 | 28 | func SignupService( 29 | Db PuppyDb, 30 | signup_channel chan string, 31 | mail_channel chan User) { 32 | 33 | for id := range signup_channel { 34 | log.Println("Signing up: " + id) 35 | 36 | u := User{} 37 | 38 | // If no such user 39 | if err := Db.GetById("user", id).One(&u); err != nil { 40 | log.Print(err) 41 | continue 42 | } 43 | 44 | // If user has already been computed 45 | if u.Dirty == false { 46 | log.Print("User ", id, " is not dirty. Skipping.") 47 | 48 | // Mailing should be async 49 | go func(user User) { 50 | log.Println("Sending mail now") 51 | mail_channel <- user 52 | }(u) 53 | 54 | continue 55 | } 56 | 57 | // Mark user as not dirty 58 | if _, err := Db.GetById("user", id). 59 | Apply(MarkNotDirtyAlt(u), &u); err != nil { 60 | 61 | log.Println("ERROR: Could not mark ", id, " as not dirty") 62 | log.Println(err) 63 | } 64 | 65 | // Mailing should be async 66 | go func(user User) { 67 | mail_channel <- user 68 | log.Println("Sending mail now") 69 | }(u) 70 | } 71 | } 72 | 73 | func MailerService(Db PuppyDb, mail_channel chan User) { 74 | mailCounter := 0 75 | auth := smtp.PlainAuth("", EmailUser, EmailPass, 76 | EmailHost) 77 | 78 | for u := range mail_channel { 79 | log.Println("Setting up smtp") 80 | 81 | to := []string{u.Email + "@iitk.ac.in"} 82 | msg := []byte("To: " + u.Email + "@iitk.ac.in" + "\r\n" + 83 | "Subject: Puppy-Love authentication code\r\n" + 84 | "\r\n" + 85 | "Use this token while signing up, and don't share it with anyone.\n" + 86 | "Token: " + u.AuthC + "\n" + 87 | ".\r\n") 88 | err := smtp.SendMail(EmailHost+":"+EmailPort, auth, 89 | EmailUser, to, msg) 90 | 91 | if err != nil { 92 | log.Println("ERROR: while mailing user ", u.Email, " ", u.Id) 93 | log.Println(err) 94 | } else { 95 | mailCounter += 1 96 | log.Println("Mailed " + u.Id) 97 | log.Println("Mails sent since inception", mailCounter) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | -------------------------------------------------------------------------------- /scripts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "bson": { 6 | "version": "1.0.4", 7 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", 8 | "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" 9 | }, 10 | "camelcase": { 11 | "version": "4.1.0", 12 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", 13 | "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" 14 | }, 15 | "mongodb": { 16 | "version": "3.0.2", 17 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.2.tgz", 18 | "integrity": "sha512-E50FmpSQchZAimn2uPIegoNoH9UQYR1yiGHtQPmmg8/Ekc97w6owHoqaBoz+assnd9V5LxMzmQ/VEWMsQMgZhQ==", 19 | "requires": { 20 | "mongodb-core": "3.0.2" 21 | } 22 | }, 23 | "mongodb-core": { 24 | "version": "3.0.2", 25 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.0.2.tgz", 26 | "integrity": "sha512-p1B0qwFQUw6C1OlFJnrOJp8KaX7MuGoogRbTaupRt0y+pPRkMllHWtE9V6i1CDtTvI3/3sy2sQwqWez7zuXEAA==", 27 | "requires": { 28 | "bson": "1.0.4", 29 | "require_optional": "1.0.1" 30 | } 31 | }, 32 | "password-generator": { 33 | "version": "2.2.0", 34 | "resolved": "https://registry.npmjs.org/password-generator/-/password-generator-2.2.0.tgz", 35 | "integrity": "sha1-/HXP95URCSPgVKWnFiNDMkC/Xkk=", 36 | "requires": { 37 | "yargs-parser": "8.1.0" 38 | } 39 | }, 40 | "require_optional": { 41 | "version": "1.0.1", 42 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 43 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 44 | "requires": { 45 | "resolve-from": "2.0.0", 46 | "semver": "5.5.0" 47 | } 48 | }, 49 | "resolve-from": { 50 | "version": "2.0.0", 51 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 52 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 53 | }, 54 | "semver": { 55 | "version": "5.5.0", 56 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", 57 | "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" 58 | }, 59 | "yargs-parser": { 60 | "version": "8.1.0", 61 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", 62 | "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", 63 | "requires": { 64 | "camelcase": "4.1.0" 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/app/crypto.ts: -------------------------------------------------------------------------------- 1 | export class Crypto { 2 | 3 | static hash(data: string) { 4 | const bits = sjcl.hash.sha256.hash(data); 5 | return sjcl.codec.hex.fromBits(bits); 6 | } 7 | 8 | static toJson(data: string) { 9 | return JSON.parse(data); 10 | } 11 | 12 | static fromJson(data) { 13 | return JSON.stringify(data); 14 | } 15 | 16 | static getRand(cnt = 5): string { 17 | return sjcl.codec.hex.fromBits(sjcl.random.randomWords(cnt)); 18 | } 19 | 20 | constructor(private password?: string, 21 | private pubK?: sjcl.SjclElGamalPublicKey, 22 | private priK?: sjcl.SjclElGamalSecretKey) { 23 | } 24 | 25 | // Key creation and storage 26 | // ------------------------ 27 | newKey() { 28 | // 6 => Paranoia value 29 | const pair = sjcl.ecc.elGamal.generateKeys(256, 6); 30 | this.priK = pair.sec; 31 | this.pubK = pair.pub; 32 | } 33 | 34 | diffieHellman(pub: string): string { 35 | let pp = new sjcl.ecc.elGamal.publicKey( 36 | sjcl.ecc.curves.c256, 37 | sjcl.codec.base64.toBits(pub) 38 | ); 39 | return sjcl.codec.hex.fromBits(this.priK.dh(pp)); 40 | } 41 | 42 | serializePub(): string { 43 | const pub = this.pubK.get(); 44 | return sjcl.codec.base64.fromBits(pub.x.concat(pub.y)); 45 | } 46 | 47 | deserializePub(serializedVal: string) { 48 | this.pubK = new sjcl.ecc.elGamal.publicKey( 49 | sjcl.ecc.curves.c256, 50 | sjcl.codec.base64.toBits(serializedVal) 51 | ); 52 | } 53 | 54 | serializePriv(): string { 55 | return sjcl.codec.base64.fromBits(this.priK.get()); 56 | } 57 | 58 | deserializePriv(sec: string) { 59 | let bitsec = sjcl.codec.base64.toBits(sec); 60 | this.priK = new sjcl.ecc.elGamal.secretKey( 61 | sjcl.ecc.curves.c256, 62 | sjcl.ecc.curves.c256.field.fromBits(bitsec) 63 | ); 64 | } 65 | 66 | // Symmetric enc and dec 67 | // --------------------- 68 | encryptSym(data: string) { 69 | return sjcl.json.encrypt(this.password, data); 70 | } 71 | 72 | decryptSym(data: sjcl.SjclCipherEncrypted) { 73 | try { 74 | return sjcl.json.decrypt(this.password, data) || '{}'; 75 | } catch (e) { 76 | return '{}'; 77 | } 78 | } 79 | 80 | // Asymmetric enc and dec 81 | // ---------------------- 82 | encryptAsym(data: string): sjcl.SjclCipherEncrypted { 83 | if (!this.pubK) { 84 | console.error('Using non-existing public key'); 85 | return; 86 | } 87 | 88 | return sjcl.encrypt(this.pubK, data); 89 | } 90 | 91 | 92 | decryptAsym(data: sjcl.SjclCipherEncrypted): string | undefined { 93 | if (!this.priK) { 94 | console.error('Decryption requires private key'); 95 | return undefined; 96 | } 97 | 98 | try { 99 | return sjcl.decrypt(this.priK, data); 100 | } catch (e) { 101 | return undefined; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /frontend/src/app/components/search/search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; 2 | import { MatInput } from '@angular/material/input'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { Subject } from 'rxjs/Subject'; 5 | import 'rxjs/add/operator/debounceTime'; 6 | import 'rxjs/add/operator/distinctUntilChanged'; 7 | 8 | @Component({ 9 | selector: 'puppy-search', 10 | templateUrl: './search.component.html', 11 | styleUrls: ['./search.component.css'] 12 | }) 13 | export class SearchComponent implements OnInit { 14 | 15 | @Input() 16 | students: any[]; 17 | @ViewChild('searchBox', { read: MatInput }) searchBox: MatInput; 18 | @Output() 19 | select = new EventEmitter(); 20 | 21 | result = []; 22 | 23 | private latestTerm = ''; 24 | private searchTerms = new Subject(); 25 | 26 | constructor() {} 27 | 28 | searchTerm(t: string): void { 29 | const term = t.trim(); 30 | this.searchTerms.next(term); 31 | } 32 | 33 | onlyUnique(value: T, index: number, self: Array): boolean { 34 | return self.indexOf(value) === index; 35 | } 36 | 37 | ngOnInit(): void { 38 | 39 | this.searchTerms 40 | .debounceTime(300) // wait 300ms after each keystroke before considering the term 41 | .distinctUntilChanged() // ignore if next search term is same as previous 42 | .subscribe(term => { 43 | this.latestTerm = term; 44 | this.update(); 45 | }); 46 | 47 | } 48 | 49 | isAny(arr: Array) { 50 | return (arr.length === 0 || 51 | (arr.length === 1 && arr[0] === 'Any')); 52 | } 53 | 54 | update(): void { 55 | if (this.latestTerm.length > 2) { 56 | this.result = this.getResults(this.students, this.latestTerm); 57 | } else { 58 | this.result = []; 59 | } 60 | } 61 | 62 | 63 | getResults(students: any[], term: string): any[] { 64 | 65 | const escape = (s: string) => { 66 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 67 | }; 68 | 69 | const filter = (elem): boolean => { 70 | 71 | if (!(term === null || term === '')) { 72 | const termregex = new RegExp(escape(term).replace(/\s+/g, ' '), 'i'); 73 | return (termregex.test(elem._id) || termregex.test(elem.email) || termregex.test(elem.name.replace(/\s+/g, ' '))); 74 | } 75 | 76 | return true; 77 | 78 | }; 79 | 80 | // Use forloop instead of filter 81 | // see https://jsperf.com/javascript-filter-vs-loop 82 | // return students.filter(filter); 83 | const resultArray = []; 84 | for (let i = 0; i < students.length; i++) { 85 | const student = students[i]; 86 | if (filter(student)) { 87 | resultArray.push(student); 88 | } 89 | } 90 | return resultArray; 91 | } 92 | 93 | onSelect(event: any) { 94 | this.searchTerms.next(''); 95 | this.select.emit(event); 96 | (this.searchBox as any)._elementRef.nativeElement.value = ''; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/pclubiitk/puppy-love/utils" 5 | 6 | "gopkg.in/mgo.v2" 7 | "gopkg.in/mgo.v2/bson" 8 | ) 9 | 10 | type ( 11 | // User represents the structure of our resource 12 | User struct { 13 | Id string `json:"_id" bson:"_id"` 14 | Name string `json:"name" bson:"name"` 15 | Email string `json:"email" bson:"email"` 16 | Gender string `json:"gender" bson:"gender"` 17 | Image string `json:"image" bson:"image"` 18 | Pass string `json:"passHash" bson:"passHash"` 19 | PrivK string `json:"privKey" bson:"privKey"` 20 | PubK string `json:"pubKey" bson:"pubKey"` 21 | AuthC string `json:"authCode" bson:"authCode"` 22 | Data string `json:"data" bson:"data"` 23 | Submit bool `json:"submitted" bson:"submitted"` 24 | Matches string `json:"matches" bson:"matches"` 25 | Vote int `json:"voted" bson:"voted"` 26 | Dirty bool `json:"dirty" bson:"dirty"` 27 | SPass string `json:"savepass" bson:"savepass"` 28 | } 29 | ) 30 | 31 | // ---------------------------------------- 32 | type TypeUserNew struct { 33 | Id string `json:"roll"` 34 | Name string `json:"name"` 35 | Email string `json:"email"` 36 | Gender string `json:"gender"` 37 | Image string `json:"image"` 38 | PassHash string `json:"passHash"` 39 | } 40 | 41 | func NewUser(info *TypeUserNew) User { 42 | return User{ 43 | Id: info.Id, 44 | Name: info.Name, 45 | Email: info.Email, 46 | Gender: info.Gender, 47 | Image: info.Image, 48 | Pass: info.PassHash, 49 | PrivK: "", 50 | PubK: "", 51 | AuthC: utils.RandStringRunes(15), 52 | Data: "", 53 | Submit: false, 54 | Matches: "", 55 | Vote: 0, 56 | Dirty: true, 57 | SPass: "", 58 | } 59 | } 60 | 61 | // ---------------------------------------- 62 | type TypeUserFirst struct { 63 | Id string `json:"roll"` 64 | AuthCode string `json:"authCode"` 65 | PassHash string `json:"passHash"` 66 | PubKey string `json:"pubKey"` 67 | PrivKey string `json:"privKey"` 68 | Data string `json:"data"` 69 | } 70 | 71 | func (u User) FirstLogin(info *TypeUserFirst) mgo.Change { 72 | return mgo.Change{ 73 | Update: bson.M{"$set": bson.M{ 74 | "authCode": "", 75 | "passHash": info.PassHash, 76 | "pubKey": info.PubKey, 77 | "privKey": info.PrivKey, 78 | "data": info.Data, 79 | "matches": "", 80 | }}, 81 | ReturnNew: true, 82 | } 83 | } 84 | 85 | // ---------------------------------------- 86 | func (u User) ValidPass(pass string) bool { 87 | return u.Pass == pass 88 | } 89 | 90 | func (u User) SetField(field string, value interface{}) mgo.Change { 91 | return mgo.Change{ 92 | Update: bson.M{"$set": bson.M{ 93 | field: value, 94 | }}, 95 | ReturnNew: true, 96 | } 97 | } 98 | 99 | type HeartsAndChoices struct { 100 | Hearts []GotHeart `json:"hearts"` 101 | Tokens Declare `json:"tokens"` 102 | } 103 | -------------------------------------------------------------------------------- /controllers/stats.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/pclubiitk/puppy-love/models" 9 | "gopkg.in/mgo.v2/bson" 10 | ) 11 | 12 | // GetStats returns useful statistics 13 | func GetStats(c *gin.Context) { 14 | var users []models.User 15 | var hearts []models.Heart 16 | if err := Db.GetCollection("user").Find(bson.M{"dirty": false}).All(&users); err != nil { 17 | c.String(http.StatusInternalServerError, "Could not get database info") 18 | return 19 | } 20 | if err := Db.GetCollection("heart").Find(nil).All(&hearts); err != nil { 21 | c.String(http.StatusInternalServerError, "Could not get database info") 22 | return 23 | } 24 | 25 | var y18males, y17males, y16males, y15males, othermales int 26 | var y18females, y17females, y16females, y15females, otherfemales int 27 | 28 | for _, user := range users { 29 | if user.Gender == "1" { 30 | if strings.HasPrefix(user.Id, "18") { 31 | y18males++ 32 | } else if strings.HasPrefix(user.Id, "17") { 33 | y17males++ 34 | } else if strings.HasPrefix(user.Id, "16") { 35 | y16males++ 36 | } else if strings.HasPrefix(user.Id, "15") { 37 | y15males++ 38 | } else { 39 | othermales++ 40 | } 41 | } else { 42 | if strings.HasPrefix(user.Id, "18") { 43 | y18females++ 44 | } else if strings.HasPrefix(user.Id, "17") { 45 | y17females++ 46 | } else if strings.HasPrefix(user.Id, "16") { 47 | y16females++ 48 | } else if strings.HasPrefix(user.Id, "15") { 49 | y15females++ 50 | } else { 51 | otherfemales++ 52 | } 53 | } 54 | } 55 | 56 | var y18maleHearts, y17maleHearts, y16maleHearts, y15maleHearts, othermaleHearts int 57 | var y18femaleHearts, y17femaleHearts, y16femaleHearts, y15femaleHearts, otherfemaleHearts int 58 | 59 | for _, heart := range hearts { 60 | if heart.Gender == "1" { 61 | if strings.HasPrefix(heart.Id, "18") { 62 | y18maleHearts++ 63 | } else if strings.HasPrefix(heart.Id, "17") { 64 | y17maleHearts++ 65 | } else if strings.HasPrefix(heart.Id, "16") { 66 | y16maleHearts++ 67 | } else if strings.HasPrefix(heart.Id, "15") { 68 | y15maleHearts++ 69 | } else { 70 | othermaleHearts++ 71 | } 72 | } else { 73 | if strings.HasPrefix(heart.Id, "18") { 74 | y18femaleHearts++ 75 | } else if strings.HasPrefix(heart.Id, "17") { 76 | y17femaleHearts++ 77 | } else if strings.HasPrefix(heart.Id, "16") { 78 | y16femaleHearts++ 79 | } else if strings.HasPrefix(heart.Id, "15") { 80 | y15femaleHearts++ 81 | } else { 82 | otherfemaleHearts++ 83 | } 84 | } 85 | } 86 | 87 | c.JSON(http.StatusOK, gin.H{ 88 | "users": len(users), 89 | "y18males": y18males, 90 | "y17males": y17males, 91 | "y16males": y16males, 92 | "y15males": y15males, 93 | "othermales": othermales, 94 | "y18females": y18females, 95 | "y17females": y17females, 96 | "y16females": y16females, 97 | "y15females": y15females, 98 | "otherfemales": otherfemales, 99 | "y18maleHearts": y18maleHearts, 100 | "y17maleHearts": y17maleHearts, 101 | "y16maleHearts": y16maleHearts, 102 | "y15maleHearts": y15maleHearts, 103 | "othermaleHearts": othermaleHearts, 104 | "y18femaleHearts": y18femaleHearts, 105 | "y17femaleHearts": y17femaleHearts, 106 | "y16femaleHearts": y16femaleHearts, 107 | "y15femaleHearts": y15femaleHearts, 108 | "otherfemaleHearts": otherfemaleHearts, 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs", 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": [ 26 | true, 27 | "spaces" 28 | ], 29 | "interface-over-type-literal": true, 30 | "label-position": true, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-access": false, 36 | "member-ordering": [ 37 | true, 38 | { 39 | "order": [ 40 | "static-field", 41 | "instance-field", 42 | "static-method", 43 | "instance-method" 44 | ] 45 | } 46 | ], 47 | "no-arg": true, 48 | "no-bitwise": true, 49 | "no-console": [ 50 | true, 51 | "debug", 52 | "info", 53 | "time", 54 | "timeEnd", 55 | "trace" 56 | ], 57 | "no-construct": true, 58 | "no-debugger": true, 59 | "no-duplicate-super": true, 60 | "no-empty": false, 61 | "no-empty-interface": true, 62 | "no-eval": true, 63 | "no-inferrable-types": [ 64 | true, 65 | "ignore-params" 66 | ], 67 | "no-misused-new": true, 68 | "no-non-null-assertion": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "directive-selector": [ 121 | true, 122 | "attribute", 123 | "puppy", 124 | "camelCase" 125 | ], 126 | "component-selector": [ 127 | true, 128 | "element", 129 | "puppy", 130 | "kebab-case" 131 | ], 132 | "no-output-on-prefix": true, 133 | "use-input-property-decorator": true, 134 | "use-output-property-decorator": true, 135 | "use-host-property-decorator": true, 136 | "no-input-rename": true, 137 | "no-output-rename": true, 138 | "use-life-cycle-interface": true, 139 | "use-pipe-transform-interface": true, 140 | "component-class-suffix": true, 141 | "directive-class-suffix": true 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /frontend/src/app/components/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DomSanitizer } from '@angular/platform-browser'; 3 | import { MatChipInputEvent } from '@angular/material'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { ENTER, COMMA } from '@angular/cdk/keycodes'; 6 | 7 | import { MainService } from '../../services/main.service'; 8 | 9 | function ImageURL(rollnum: string, userid: string) { 10 | const iitkhome = `http://home.iitk.ac.in/~${ userid }/dp`; 11 | const oaimage = `https://oa.cc.iitk.ac.in/Oa/Jsp/Photo/${ rollnum }_0.jpg`; 12 | return `url("${ iitkhome }"), url("${ oaimage }")`; 13 | } 14 | 15 | @Component({ 16 | selector: 'puppy-home', 17 | templateUrl: './home.component.html', 18 | styleUrls: [ './home.component.scss' ] 19 | }) 20 | export class HomeComponent implements OnInit { 21 | 22 | // Enter, comma 23 | separatorKeysCodes = [ENTER, COMMA]; 24 | 25 | user$: any; 26 | 27 | constructor(private main: MainService, 28 | private sanitizer: DomSanitizer, 29 | private snackbar: MatSnackBar) {} 30 | 31 | ngOnInit() { 32 | this.user$ = this.main.user$; 33 | this.doSubmit(); 34 | } 35 | 36 | get url() { 37 | const currentUser = { 38 | ...this.main.user$.value 39 | }; 40 | return this.sanitizer.bypassSecurityTrustStyle(ImageURL(currentUser._id, currentUser.email)); 41 | } 42 | 43 | maleHearts(user) { 44 | return user.data.received.filter((x) => x.genderOfSender === '1'); 45 | } 46 | 47 | femaleHearts(user) { 48 | return user.data.received.filter((x) => x.genderOfSender === '0'); 49 | } 50 | 51 | add(event): void { 52 | const currentUser = { 53 | ...this.main.user$.value 54 | }; 55 | 56 | if (!event) { 57 | return; 58 | } 59 | 60 | if (event._id !== currentUser._id && !currentUser.data.choices.some((x) => x._id === event._id)) { 61 | currentUser.data.choices.push(event); 62 | this.main.user$.next(currentUser); 63 | } 64 | } 65 | 66 | // add(event: MatChipInputEvent): void { 67 | // const input = event.input; 68 | // const value = event.value; 69 | 70 | // const currentUser = { 71 | // ...this.main.user$.value 72 | // }; 73 | // if ((value || '').trim()) { 74 | // currentUser.data.choices.push({ _id: value.trim(), name: 'Foobar', email: 'foobar' }); 75 | // this.main.user$.next(currentUser); 76 | // } 77 | 78 | // // Reset the input value 79 | // if (input) { 80 | // input.value = ''; 81 | // } 82 | // } 83 | 84 | remove(fruit: any): void { 85 | const currentUser = { 86 | ...this.main.user$.value 87 | }; 88 | 89 | if (currentUser.submitted) { 90 | return; 91 | } 92 | 93 | const index = currentUser.data.choices.indexOf(fruit); 94 | 95 | if (index >= 0) { 96 | currentUser.data.choices.splice(index, 1); 97 | this.main.user$.next(currentUser); 98 | } 99 | } 100 | 101 | doSubmit() { 102 | const user = this.user$.value; 103 | if (user.submitted) { 104 | this.main.submit().subscribe( 105 | () => console.log('Autosubmission.'), 106 | (error) => this.snackbar.open('An error occurred: ' + error, '', { duration: 3000 }) 107 | ); 108 | } 109 | } 110 | 111 | onSubmit() { 112 | if(!window.confirm('This will finalize your choices, you cannot change them afterwards. Proceed?')) { 113 | // If you've seen this, you can assume that you've understood all the code here. 114 | return; 115 | } 116 | this.snackbar.open('Submitting, please wait...'); 117 | this.main.submit().subscribe( 118 | () => this.snackbar.open('Submitted.', '', { duration: 3000 }), 119 | (error) => this.snackbar.open('An error occurred: ' + error, '', { duration: 3000 }) 120 | ); 121 | } 122 | 123 | onSave() { 124 | this.snackbar.open('Saving your info, please wait...'); 125 | this.main.save().then(() => this.snackbar.open('Saved your info.', '', { duration: 3000 })); 126 | } 127 | 128 | onLogout() { 129 | location.reload(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/boj/redistore" 6 | packages = ["."] 7 | revision = "fc113767cd6b051980f260d6dbe84b2740c46ab0" 8 | version = "v1.2" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/bradfitz/gomemcache" 13 | packages = ["memcache"] 14 | revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b" 15 | 16 | [[projects]] 17 | branch = "master" 18 | name = "github.com/bradleypeabody/gorilla-sessions-memcache" 19 | packages = ["."] 20 | revision = "75ee37df8664a47cd5d9eb84d41e1f2b08086893" 21 | 22 | [[projects]] 23 | name = "github.com/garyburd/redigo" 24 | packages = [ 25 | "internal", 26 | "redis" 27 | ] 28 | revision = "d1ed5c67e5794de818ea85e6b522fda02623a484" 29 | version = "v1.4.0" 30 | 31 | [[projects]] 32 | branch = "master" 33 | name = "github.com/gin-contrib/sessions" 34 | packages = ["."] 35 | revision = "cccdeef56346e7037ca92de250c2b55ef5e7ffe3" 36 | 37 | [[projects]] 38 | branch = "master" 39 | name = "github.com/gin-contrib/sse" 40 | packages = ["."] 41 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae" 42 | 43 | [[projects]] 44 | name = "github.com/gin-gonic/gin" 45 | packages = [ 46 | ".", 47 | "binding", 48 | "render" 49 | ] 50 | revision = "d459835d2b077e44f7c9b453505ee29881d5d12d" 51 | version = "v1.2" 52 | 53 | [[projects]] 54 | branch = "master" 55 | name = "github.com/golang/protobuf" 56 | packages = ["proto"] 57 | revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845" 58 | 59 | [[projects]] 60 | name = "github.com/gorilla/context" 61 | packages = ["."] 62 | revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" 63 | version = "v1.1" 64 | 65 | [[projects]] 66 | name = "github.com/gorilla/securecookie" 67 | packages = ["."] 68 | revision = "667fe4e3466a040b780561fe9b51a83a3753eefc" 69 | version = "v1.1" 70 | 71 | [[projects]] 72 | name = "github.com/gorilla/sessions" 73 | packages = ["."] 74 | revision = "ca9ada44574153444b00d3fd9c8559e4cc95f896" 75 | version = "v1.1" 76 | 77 | [[projects]] 78 | name = "github.com/imdario/mergo" 79 | packages = ["."] 80 | revision = "163f41321a19dd09362d4c63cc2489db2015f1f4" 81 | version = "0.3.2" 82 | 83 | [[projects]] 84 | name = "github.com/kataras/go-errors" 85 | packages = ["."] 86 | revision = "d2aaffdebcb40c69c4cb6a95e436430cde192435" 87 | version = "v0.0.3" 88 | 89 | [[projects]] 90 | branch = "master" 91 | name = "github.com/kidstuff/mongostore" 92 | packages = ["."] 93 | revision = "256d65ac5b0e35e7c5ebb3f175c0bed1e5c2b253" 94 | 95 | [[projects]] 96 | name = "github.com/mattn/go-isatty" 97 | packages = ["."] 98 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" 99 | version = "v0.0.3" 100 | 101 | [[projects]] 102 | branch = "master" 103 | name = "github.com/ugorji/go" 104 | packages = ["codec"] 105 | revision = "9831f2c3ac1068a78f50999a30db84270f647af6" 106 | 107 | [[projects]] 108 | branch = "master" 109 | name = "golang.org/x/sys" 110 | packages = ["unix"] 111 | revision = "2c42eef0765b9837fbdab12011af7830f55f88f0" 112 | 113 | [[projects]] 114 | name = "gopkg.in/go-playground/validator.v8" 115 | packages = ["."] 116 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" 117 | version = "v8.18.2" 118 | 119 | [[projects]] 120 | name = "gopkg.in/kataras/go-sessions.v0" 121 | packages = [ 122 | "sessiondb/redis", 123 | "sessiondb/redis/service" 124 | ] 125 | revision = "d7ae81e3f9a643facd6f210f3bb1dc3674302b10" 126 | version = "v0.0.7" 127 | 128 | [[projects]] 129 | branch = "v2" 130 | name = "gopkg.in/mgo.v2" 131 | packages = [ 132 | ".", 133 | "bson", 134 | "internal/json", 135 | "internal/sasl", 136 | "internal/scram" 137 | ] 138 | revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655" 139 | 140 | [[projects]] 141 | branch = "v2" 142 | name = "gopkg.in/yaml.v2" 143 | packages = ["."] 144 | revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" 145 | 146 | [solve-meta] 147 | analyzer-name = "dep" 148 | analyzer-version = 1 149 | inputs-digest = "7912ee9fb8066fad398d9fc2c87f8ba0d66badbf628e7443c8f37c42667c097b" 150 | solver-name = "gps-cdcl" 151 | solver-version = 1 152 | -------------------------------------------------------------------------------- /frontend/src/app/components/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 |
6 | 7 |

Hello, {{ user.name }}

8 |
9 | 10 | 11 |
12 |
13 | Hi {{ user.name }} 14 | 15 | 19 | 23 |
24 | About the platform 25 | Source Code 26 | pclub.in 27 |
28 |
29 | 30 | 31 | 32 | 35 | 36 |
37 |

You can only select 4 people.

38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |

Your choices:

56 |
(Click to remove)
57 |
58 |
59 | 60 |
61 | 65 | 69 |
70 |
71 |

Hearts from people that the OA identifies as male:

72 |
73 |
74 | 75 |
76 |
77 |

Hearts from people that the OA identifies as female:

78 |
79 |
80 | 81 |
82 |
84 |

Sorry, you haven't gotten any hearts (yet).

85 |
True love comes to those who wait.
86 |
87 | 88 |
89 |
90 |
91 |
92 | 93 | 94 | Loading 95 | 96 | -------------------------------------------------------------------------------- /frontend/src/app/components/results/results.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DomSanitizer } from '@angular/platform-browser'; 3 | import { MatSnackBar } from '@angular/material/snack-bar'; 4 | 5 | import { of } from 'rxjs/observable/of'; 6 | import { catchError, switchMap } from 'rxjs/operators'; 7 | import { MainService, Stats } from '../../services/main.service'; 8 | 9 | function ImageURL(rollnum: string, userid: string) { 10 | const iitkhome = `http://home.iitk.ac.in/~${ userid }/dp`; 11 | const oaimage = `https://oa.cc.iitk.ac.in/Oa/Jsp/Photo/${ rollnum }_0.jpg`; 12 | return `url("${ iitkhome }"), url("${ oaimage }")`; 13 | } 14 | 15 | @Component({ 16 | selector: 'puppy-results', 17 | templateUrl: './results.component.html', 18 | styleUrls: [ './results.component.scss' ] 19 | }) 20 | export class ResultsComponent implements OnInit { 21 | 22 | user$: any; 23 | matches: any[]; 24 | stats: Stats; 25 | 26 | colorScheme = { 27 | domain: ['#C2024F', '#04BBBF', '#D2D945', '#FCB13F', '#FF594F'] 28 | }; 29 | 30 | constructor(private main: MainService, 31 | private sanitizer: DomSanitizer, 32 | private snackbar: MatSnackBar) {} 33 | 34 | ngOnInit() { 35 | this.user$ = this.main.user$; 36 | this.doSubmit(); 37 | this.getStats(); 38 | } 39 | 40 | get url() { 41 | const currentUser = { 42 | ...this.main.user$.value 43 | }; 44 | return this.sanitizer.bypassSecurityTrustStyle(ImageURL(currentUser._id, currentUser.email)); 45 | } 46 | 47 | get registrations() { 48 | const stats = this.stats; 49 | const totalMales = stats.othermales + stats.y15males + stats.y16males + stats.y17males + stats.y18males; 50 | const totalFemales = stats.otherfemales + stats.y15females + stats.y16females + stats.y17females + stats.y18females; 51 | return [{ 52 | name: 'Males', 53 | value: totalMales, 54 | }, { 55 | name: 'Females', 56 | value: totalFemales, 57 | }]; 58 | } 59 | 60 | get hearts() { 61 | const stats = this.stats; 62 | const totalMaleHearts = stats.othermaleHearts + stats.y15maleHearts + stats.y16maleHearts + stats.y17maleHearts + stats.y18maleHearts; 63 | const totalFemaleHearts = stats.otherfemaleHearts + stats.y15femaleHearts + stats.y16femaleHearts + stats.y17femaleHearts + stats.y18femaleHearts; 64 | return [{ 65 | name: 'Males', 66 | value: totalMaleHearts, 67 | }, { 68 | name: 'Females', 69 | value: totalFemaleHearts, 70 | }]; 71 | } 72 | get fhearts() { 73 | const stats = this.stats; 74 | return [{ 75 | name: 'Others', 76 | value: stats.otherfemaleHearts, 77 | }, { 78 | name: 'Y15', 79 | value: stats.y15femaleHearts, 80 | }, { 81 | name: 'Y16', 82 | value: stats.y16femaleHearts, 83 | }, { 84 | name: 'Y17', 85 | value: stats.y17femaleHearts, 86 | }, { 87 | name: 'Y18', 88 | value: stats.y18femaleHearts, 89 | }].reverse(); 90 | 91 | } 92 | 93 | get mhearts() { 94 | const stats = this.stats; 95 | return [{ 96 | name: 'Others', 97 | value: stats.othermaleHearts, 98 | }, { 99 | name: 'Y15', 100 | value: stats.y15maleHearts, 101 | }, { 102 | name: 'Y16', 103 | value: stats.y16maleHearts, 104 | }, { 105 | name: 'Y17', 106 | value: stats.y17maleHearts, 107 | }, { 108 | name: 'Y18', 109 | value: stats.y18maleHearts, 110 | }].reverse(); 111 | } 112 | 113 | get fregs() { 114 | const stats = this.stats; 115 | return [{ 116 | name: 'Others', 117 | value: stats.otherfemales, 118 | }, { 119 | name: 'Y15', 120 | value: stats.y15females, 121 | }, { 122 | name: 'Y16', 123 | value: stats.y16females, 124 | }, { 125 | name: 'Y17', 126 | value: stats.y17females, 127 | }, { 128 | name: 'Y18', 129 | value: stats.y18females, 130 | }].reverse(); 131 | 132 | } 133 | 134 | get mregs() { 135 | const stats = this.stats; 136 | return [{ 137 | name: 'Others', 138 | value: stats.othermales, 139 | }, { 140 | name: 'Y15', 141 | value: stats.y15males, 142 | }, { 143 | name: 'Y16', 144 | value: stats.y16males, 145 | }, { 146 | name: 'Y17', 147 | value: stats.y17males, 148 | }, { 149 | name: 'Y18', 150 | value: stats.y18males, 151 | }].reverse(); 152 | } 153 | 154 | maleHearts(user) { 155 | return user.data.received.filter((x) => x.genderOfSender === '1'); 156 | } 157 | 158 | femaleHearts(user) { 159 | return user.data.received.filter((x) => x.genderOfSender === '0'); 160 | } 161 | 162 | doSubmit() { 163 | const user = this.user$.value; 164 | this.main.submit().pipe( 165 | catchError((err) => of(console.error(err))), 166 | switchMap(() => this.main.matches()), 167 | ).subscribe( 168 | (match) => { 169 | if (match.matches === '') { 170 | this.matches = []; 171 | } else { 172 | this.matches = match.matches.split(' ').map(x => this.main.people.filter(p => p._id === x)[0]); 173 | } 174 | }, 175 | (error) => this.snackbar.open(error, '', { duration: 3000 }) 176 | ); 177 | } 178 | 179 | getStats() { 180 | this.main.stats().subscribe((x) => this.stats = x); 181 | } 182 | 183 | onLogout() { 184 | location.reload(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /service/results/results.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sort" 8 | "strings" 9 | 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | type Declare struct { 14 | Id string `json:"_id" bson:"_id"` 15 | Token0 string `json:"t0" bson:"t0"` 16 | Token1 string `json:"t1" bson:"t1"` 17 | Token2 string `json:"t2" bson:"t2"` 18 | Token3 string `json:"t3" bson:"t3"` 19 | } 20 | 21 | type Flat struct { 22 | Id string `json:"_id" bson:"_id"` 23 | Token string `json:"t" bson:"t"` 24 | } 25 | 26 | type ByValue []Flat 27 | 28 | func (b ByValue) Len() int { 29 | return len(b) 30 | } 31 | func (b ByValue) Swap(i, j int) { 32 | b[i], b[j] = b[j], b[i] 33 | } 34 | func (b ByValue) Less(i, j int) bool { 35 | return b[i].Token < b[j].Token 36 | } 37 | 38 | func AddMatch(toAdd string, prev string) string { 39 | if prev == "" { 40 | return toAdd 41 | } 42 | 43 | return prev + " " + toAdd 44 | } 45 | 46 | func main() { 47 | CfgInit() 48 | log.Println("Computing for table:", CfgTable) 49 | db, error := MongoConnect() 50 | if error != nil { 51 | log.Print("ERROR: Could not connect to MongoDB") 52 | os.Exit(1) 53 | } 54 | 55 | var decs []Declare 56 | collection := db.GetCollection(CfgTable) 57 | err := collection.Find(bson.M{}).All(&decs) 58 | if err != nil { 59 | log.Println("ERROR: Results cannot fetch declare list: ") 60 | log.Println(err) 61 | return 62 | } 63 | 64 | var flat []Flat 65 | for _, fe := range decs { 66 | if fe.Token0 != "" { 67 | flat = append(flat, Flat{ 68 | Id: fe.Id, 69 | Token: fe.Token0, 70 | }) 71 | } 72 | if fe.Token1 != "" { 73 | flat = append(flat, Flat{ 74 | Id: fe.Id, 75 | Token: fe.Token1, 76 | }) 77 | } 78 | if fe.Token2 != "" { 79 | flat = append(flat, Flat{ 80 | Id: fe.Id, 81 | Token: fe.Token2, 82 | }) 83 | } 84 | if fe.Token3 != "" { 85 | flat = append(flat, Flat{ 86 | Id: fe.Id, 87 | Token: fe.Token3, 88 | }) 89 | } 90 | } 91 | 92 | sort.Sort(ByValue(flat)) 93 | 94 | for i := range flat { 95 | fmt.Println(flat[i].Token, flat[i].Id) 96 | } 97 | 98 | fmt.Println() 99 | 100 | var emails = []string{} 101 | var maleMale, femaleFemale, crossGender int 102 | var matches string 103 | 104 | for i := range flat { 105 | if i == 0 { 106 | continue 107 | } 108 | 109 | if flat[i].Token == flat[i-1].Token { 110 | fmt.Println("Matched:", flat[i].Id, "and", flat[i-1].Id) 111 | 112 | u1 := User{} 113 | u2 := User{} 114 | if err := db.GetById("user", flat[i].Id).One(&u1); err != nil { 115 | fmt.Println("Error") 116 | fmt.Println(err) 117 | continue 118 | } 119 | if err := db.GetById("user", flat[i-1].Id).One(&u2); err != nil { 120 | fmt.Println("Error") 121 | fmt.Println(err) 122 | continue 123 | } 124 | 125 | fmt.Println("Names: ", u1.Name, "and", u2.Name) 126 | fmt.Println("Emails: ", u1.Email, "and", u2.Email) 127 | fmt.Println("Genders: ", u1.Gender, u2.Gender) 128 | fmt.Println("Prev matches: ", u1.Matches, "; ", u2.Matches) 129 | 130 | // EXTRA STUFF, NOT NEEDED 131 | // If you want to verify some information 132 | // As an example here, I match the hash, in the hypothetical scenario 133 | // that the hash can be computed on the server 134 | 135 | //fmt.Println("Hash: ", flat[i].Token) 136 | //pairId := "" 137 | //if flat[i].Id > flat[i-1].Id { 138 | //pairId = flat[i].Id + "-" + flat[i-1].Id 139 | //} else { 140 | //pairId = flat[i-1].Id + "-" + flat[i].Id 141 | //} 142 | 143 | //out, _ := exec.Command("/bin/bash", "-c", "echo -n '"+pairId+"' | sha256sum").Output() 144 | //h2 := strings.Fields(string(out[:]))[0] 145 | //fmt.Println("Has2: ", h2) 146 | //if h2 != flat[i].Token { 147 | //fmt.Println("ERROR") 148 | //continue 149 | //} else { 150 | //cnt = cnt + 1 151 | //} 152 | 153 | // if u1.Gender == u2.Gender { 154 | // fmt.Println("SAME GENDER") 155 | // fmt.Println("ERROR") 156 | // continue 157 | // } 158 | 159 | if u1.Gender != u2.Gender { 160 | crossGender++ 161 | } else if u1.Gender == "1" { 162 | maleMale++ 163 | } else { 164 | femaleFemale++ 165 | } 166 | 167 | matches += fmt.Sprintf("%s(%s) <-> %s(%s)\n", u1.Name, u1.Id, u2.Name, u2.Id) 168 | 169 | emails = append(emails, u1.Email) 170 | emails = append(emails, u2.Email) 171 | 172 | var b bool 173 | var ans string 174 | 175 | p1matches := strings.Fields(u1.Matches) 176 | b = false 177 | for _, match := range p1matches { 178 | if match == u2.Id { 179 | b = true 180 | break 181 | } 182 | } 183 | if !b { 184 | fmt.Println(u2.Name, "is not in matches of", u1.Name, ". Add?") 185 | // _, _ = fmt.Scanf("%s", &ans) 186 | ans = "y" 187 | if ans == "y" { 188 | fmt.Println("Adding", AddMatch(u2.Id, u1.Matches)) 189 | 190 | if _, err := db.GetById("user", u1.Id). 191 | Apply(u1.MatchedUpdate(AddMatch(u2.Id, u1.Matches)), &u1); err != nil { 192 | fmt.Println(err) 193 | } 194 | 195 | } else { 196 | fmt.Println("You said no") 197 | } 198 | } 199 | 200 | p2matches := strings.Fields(u2.Matches) 201 | b = false 202 | for _, match := range p2matches { 203 | if match == u1.Id { 204 | b = true 205 | break 206 | } 207 | } 208 | if !b { 209 | fmt.Println(u1.Name, "is not in matches of", u2.Name, ". Add?") 210 | // _, _ = fmt.Scanf("%s", &ans) 211 | ans = "y" 212 | if ans == "y" { 213 | fmt.Println("Adding", AddMatch(u1.Id, u2.Matches)) 214 | 215 | if _, err := db.GetById("user", u2.Id). 216 | Apply(u2.MatchedUpdate(AddMatch(u1.Id, u2.Matches)), &u2); err != nil { 217 | fmt.Println(err) 218 | } 219 | 220 | } else { 221 | fmt.Println("You said no") 222 | } 223 | } 224 | fmt.Println("**********************************************") 225 | } 226 | } 227 | fmt.Println() 228 | for _, mail := range emails { 229 | fmt.Println(mail) 230 | } 231 | fmt.Println(matches) 232 | fmt.Println("Total", maleMale+femaleFemale+crossGender, "matches") 233 | fmt.Println("Male-Male matches: ", maleMale) 234 | fmt.Println("Female-Female matches: ", femaleFemale) 235 | fmt.Println("Male-Female matches: ", crossGender) 236 | fmt.Println() 237 | } 238 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Puppy Love 2 | ========== 3 | 4 | The modern and better avatar of Programming Club's [puppy-love](https://github.com/pclubiitk/valentine). 5 | 6 | Puppy Love is a platform for shy people to reach out anonymously to their crush. They can sign up and mark up to 4 of their crushes. On Valentine's day, the platform will match them anonymously, and will inform them if both of them like each other. 7 | 8 | Used in IIT Kanpur from 7th to 14th February, 2017, witnessing 1800+ registrants. 9 | 10 | ![alt tag](https://raw.githubusercontent.com/pclubiitk/puppy-love/master/cover.jpg) 11 | 12 | Algorithm designed from the ground up, with a completely secure computation model which guarantees the following: 13 | 14 | * The identities of your choices are never, *ever*, exposed in plain text. Not even at the server. 15 | * The server, even while matching couples, can *not* know what the choices were. 16 | * The other person will only know whether you liked him/her or not if that person liked you as well. 17 | * The server will know whether you matched with some person or not, but no more. 18 | * The above guarantees are independent of the code running on the server, and can be verified on the browser. 19 | 20 | Blog posts describing the algorithm: 21 | * [Part 1 - Introduction](https://sakshamsharma.com/2016/10/puppy1/) 22 | * [Part 2 - Matching choices](https://sakshamsharma.com/2016/11/puppy2/) 23 | * [Part 3 - Restricting number of choices to 4 anonymously](https://sakshamsharma.com/2016/12/puppy3/) 24 | 25 | Implementation using: 26 | 27 | * Golang (iris) 28 | * TypeScript 29 | * Angular2 30 | * Bootstrap 31 | * Docker 32 | * Docker Compose 33 | 34 | # Requirements 35 | * golang 36 | * NPM 37 | * Nginx 38 | * Docker 39 | 40 | # Installation / Setup 41 | 42 | ## Server deployment 43 | It is really easy. Like, **really**. 44 | 45 | ``` 46 | # You need docker-compose 1.10 or higher. 47 | # Use the release script version from their website for installation. 48 | docker-compose up 49 | 50 | # Use the following to stop a container 51 | docker-compose stop 52 | 53 | # Use the following to start a container 54 | docker-compose start 55 | 56 | # Use the following to recreate and run a container 57 | docker-compose up -d 58 | ``` 59 | 60 | ## Development environment setup 61 | You shall need docker, golang, nodejs, nginx and npm for the following steps. 62 | 63 | **Note**: Arch users often have gccgo installed. Please use the package `go` from the main repositories instead. 64 | 65 | ``` 66 | mkdir -p $HOME/go/src 67 | 68 | # Change .bashrc to .zshrc depending on your shell 69 | echo "export GOPATH=$HOME/go:$GOPATH" >> $HOME/.bashrc 70 | source $HOME/.bashrc 71 | ``` 72 | 73 | Get the source code. 74 | 75 | You can later symlink the following folder into a convenient location. 76 | Just make sure that the actual folder (not symlink) is in the go directory. 77 | ``` 78 | git clone https://github.com/pclubiitk/puppy-love $HOME/go/src/github.com/pclubiitk/puppy-love 79 | ``` 80 | 81 | Install glide 82 | We use `glide` to maintain dependencies. `go get` is not recommended. 83 | ``` 84 | curl https://glide.sh/get | sh 85 | cd $HOME/go/src/github.com/pclubiitk/puppy-love 86 | ``` 87 | 88 | Set up nginx 89 | ``` 90 | sudo cp puppy.nginx.conf /etc/nginx/sites_enabled/ 91 | ``` 92 | 93 | Edit /etc/hosts file 94 | ``` 95 | # Map dev.puppy.pclub.in to 127.0.0.1 96 | # It should have a line saying: 97 | # 127.0.0.1 dev.puppy.pclub.in 98 | ``` 99 | 100 | Get the essential dockers 101 | ``` 102 | sudo systemctl start docker 103 | docker run --name puppy-redis -p 6379:6379 -d redis 104 | docker run --name puppy-mongo-db -p 27017:27017 -d mongo 105 | 106 | # Optional (in place of the above command): 107 | # docker run --name puppy-mongo-db -p 27017:27017 -v $HOME/.mongodata:/data/db -d mongo 108 | ``` 109 | 110 | Get dependencies for backend 111 | ``` 112 | glide install 113 | ``` 114 | 115 | Get dependencies for frontend 116 | ``` 117 | cd views 118 | sudo npm install -g yarn 119 | yarn install 120 | ``` 121 | 122 | ## Run services 123 | ``` 124 | sudo systemctl start nginx 125 | 126 | # These are not required if you just finished the above steps 127 | docker start puppy-mongo-db 128 | docker start puppy-redis 129 | ``` 130 | 131 | ## Run puppy-love 132 | ### Frontend 133 | ``` 134 | # Run frontend (inside folder views) 135 | yarn start 136 | 137 | # IFF production, use 138 | yarn build && python -m http.serve 8091 139 | ``` 140 | 141 | ### Backend 142 | ``` 143 | # Build (inside puppy-love folder) 144 | go build 145 | 146 | # Optional: These are needed for email verification to work 147 | export EMAIL_USER= 148 | export EMAIL_PASS= 149 | ./puppy-love 150 | ``` 151 | 152 | # Setting up basic services 153 | ### Log in 154 | You should first log in as admin. A simple way to do that is the following: 155 | ``` 156 | cd scripts 157 | . ./login.sh admin passhash 158 | # Use curl / http normally, but use $CADMIN cookie at the end of your command 159 | # Example: http get 'localhost:3000/admin/...' $CADMIN 160 | ``` 161 | 162 | ### Add a user 163 | Note: This requires you to be logged in as $CADMIN 164 | ``` 165 | cd scripts 166 | ./newuser.sh 167 | # Follow the commands 168 | # For testing, use your own IITK email address for all users 169 | ``` 170 | 171 | ### Set up compute table 172 | Do this AFTER setting up all users. Whenever you add a new user, this has to be run. 173 | ``` 174 | http get 'localhost:3000/compute/prepare' $CADMIN 175 | ``` 176 | 177 | ### Using the frontend 178 | Once you've created the users, you will need to register them. Open the UI at dev.puppy.pclub.in, and go to signup. You can only register for users which you have created. Get your auth token via email, and then fill up the remaining fields. 179 | 180 | ### Notes 181 | * You cannot login as admin on the frontend UI. 182 | * You can also check mongoDB's data for the auth token for the user. 183 | * Doing the above will mandate marking the user as `non-dirty` in the MongoDb users table manually. 184 | 185 | You can open the local website at [dev.puppy.pclub.in](dev.puppy.pclub.in) 186 | The backend will be listening on the printed port number. 187 | 188 | ### Contributors 189 | The following people have contributed to this project in various capacities: 190 | * [Saksham Sharma](https://github.com/sakshamsharma) 191 | * [Yash Srivastav](https://github.com/yashsriv) 192 | * [Kunal Kapila](https://github.com/kunalapila) 193 | * [Vinayak Tantia](https://github.com/vtantia) 194 | * [Milind Luthra](https://github.com/milindl) 195 | -------------------------------------------------------------------------------- /frontend/src/app/components/results/results.component.html: -------------------------------------------------------------------------------- 1 | 2 |
6 | 7 |

Hello, {{ user.name }}

8 |
9 | 10 |
11 |
12 | Hi {{ user.name }} 13 | 14 | 18 |
19 | About the platform 20 | Source Code 21 | pclub.in 22 |
23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 |
32 | 33 |
34 |

Your Matches:

35 |
36 |
37 | 38 |
39 |
41 |

Sorry, even though you received a lot of love, you didn't match with anyone.

42 |
43 |
45 |

Sorry, you didn't match with anyone.

46 |
You waited, but love did not come.
47 |
48 |
49 |
50 |

Hearts from people that the OA identifies as male:

51 |
52 |
53 | 54 |
55 |
56 |

Hearts from people that the OA identifies as female:

57 |
58 |
59 | 60 |
61 |
63 |

Sorry, you haven't gotten any hearts.

64 |
True love comes to those who wait.
65 |
66 |
67 |
68 | 69 | Stats 70 | 71 |
72 |
73 |

Registrations

74 | 80 | 81 |
82 |
83 |

Female Registrations

84 | 90 | 91 |
92 |
93 |

Male Registrations

94 | 100 | 101 |
102 |
103 |

Hearts Transferred

104 | 110 | 111 |
112 |
113 |

Female Hearts

114 | 120 | 121 |
122 |
123 |

Male Hearts

124 | 130 | 131 |
132 |
133 | 134 |

Note: Males and Females refer to people identified as Male or Female by OA

135 |
136 |
137 |
138 |
139 |
140 |
141 | 142 | 143 | Loading 144 | 145 | -------------------------------------------------------------------------------- /frontend/src/app/services/main.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | 4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 5 | import { Observable } from 'rxjs/Observable'; 6 | import 'rxjs/add/observable/throw'; 7 | import { of } from 'rxjs/observable/of'; 8 | import 'rxjs/add/operator/toPromise'; 9 | import { catchError, map, switchMap, tap } from 'rxjs/operators'; 10 | 11 | import { Crypto } from '../crypto'; 12 | 13 | export interface Stats { 14 | otherfemaleHearts: number; 15 | y18femaleHearts: number; 16 | y17femaleHearts: number; 17 | y16femaleHearts: number; 18 | y15femaleHearts: number; 19 | othermaleHearts: number; 20 | y18maleHearts: number; 21 | y17maleHearts: number; 22 | y16maleHearts: number; 23 | y15maleHearts: number; 24 | otherfemales: number; 25 | y18females: number; 26 | y17females: number; 27 | y16females: number; 28 | y15females: number; 29 | othermales: number; 30 | y18males: number; 31 | y17males: number; 32 | y16males: number; 33 | y15males: number; 34 | } 35 | 36 | interface LoginData { 37 | _id: string; 38 | name: string; 39 | email: string; 40 | gender: string; 41 | privKey: sjcl.SjclCipherEncrypted; 42 | pubKey: string; 43 | data: sjcl.SjclCipherEncrypted; 44 | submitted: boolean; 45 | matches: string; 46 | } 47 | 48 | interface MatchInfo { 49 | _id: string; 50 | matches: string; 51 | } 52 | 53 | interface Person { 54 | _id: string; 55 | name: string; 56 | email: string; 57 | } 58 | 59 | interface Heart { 60 | v: string; 61 | data: string; 62 | } 63 | 64 | interface Declare { 65 | _id: string; 66 | t0: string; 67 | t1: string; 68 | t2: string; 69 | t3: string; 70 | } 71 | 72 | interface ReceivedHeart { 73 | v: sjcl.SjclCipherEncrypted; 74 | genderOfSender: string; 75 | } 76 | 77 | interface User { 78 | _id: string; 79 | name: string; 80 | email: string; 81 | gender: string; 82 | submitted: boolean; 83 | crypto: Crypto; 84 | data: { 85 | choices: Person[], 86 | hearts: Heart[], 87 | lastCheck: number, 88 | received: ReceivedHeart[], 89 | } 90 | } 91 | 92 | @Injectable() 93 | export class MainService { 94 | 95 | user$: BehaviorSubject = new BehaviorSubject(null); 96 | matches$: BehaviorSubject = new BehaviorSubject(null); 97 | loggedIn = false; 98 | 99 | pubKeys: Map = new Map(); 100 | people: Person[]; 101 | 102 | constructor(private http: HttpClient) { 103 | this.http.get('/api/list/all').subscribe((res) => { 104 | this.people = res; 105 | }); 106 | } 107 | 108 | private getInfo(username:string, password: string) { 109 | const info = this.http.get('/api/users/data/info') 110 | .pipe( 111 | map(x => { 112 | const crypto = new Crypto(password); 113 | crypto.deserializePriv(crypto.decryptSym(x.privKey)); 114 | crypto.deserializePub(x.pubKey); 115 | const data = Crypto.toJson(crypto.decryptSym(x.data)); 116 | return { 117 | _id: x._id, 118 | name: x.name, 119 | email: x.email, 120 | gender: x.gender, 121 | submitted: x.submitted, 122 | crypto, 123 | data: { 124 | choices: data.choices || [], 125 | hearts: data.hearts || [], 126 | lastCheck: data.lastCheck || 0, 127 | received: data.received || [], 128 | } 129 | }; 130 | }), 131 | tap((user) => this.user$.next(user)), 132 | tap(() => this.loggedIn = true) 133 | ); 134 | return info; 135 | } 136 | 137 | private declareChoices(pubkeys: Map) { 138 | const user = this.user$.value; 139 | let declarePayload = {'_id': user._id}; 140 | let declarevalues = []; 141 | let heartvalues = []; 142 | let cnt = 0; 143 | for (let p of user.data.choices) { 144 | if (!pubkeys.has(p._id)) { 145 | console.log('Nk for ' + p._id); 146 | cnt = cnt + 1; 147 | continue; 148 | } 149 | const pubk = pubkeys.get(p._id); 150 | 151 | // Now calculate the heart values 152 | const cry = new Crypto(); 153 | cry.deserializePub(pubk); 154 | heartvalues.push({ 155 | 'v': cry.encryptAsym(Crypto.getRand(1)), 156 | 'data': cnt.toString(), 157 | 'genderOfSender': user.gender, 158 | }); 159 | 160 | const pairId = (user._id > p._id ? (user._id + '-' + p._id) : (p._id + '-' + user._id)); 161 | declarevalues.push(Crypto.hash(pairId + '-' + user.crypto.diffieHellman(pubk))); 162 | cnt = cnt + 1; 163 | } 164 | 165 | // Trim down choices to 4 166 | let count = Math.min(4, cnt); 167 | for (let i = 0; i < count; i++) { 168 | declarePayload['t' + i] = declarevalues[i]; 169 | } 170 | for (let i = count; i < 4; i++) { 171 | declarePayload['t' + i] = ''; 172 | } 173 | 174 | return this.http.post('/api/users/data/submit/' + user._id, { 175 | hearts: heartvalues, 176 | tokens: declarePayload 177 | }).pipe ( 178 | tap(() => console.log('Saved hearts: ' + heartvalues.length + ' and declare values: ' + count)), 179 | ); 180 | 181 | } 182 | 183 | private getPubKeys() { 184 | return this.http.get('/api/list/pubkey') 185 | .pipe ( 186 | map(items => { 187 | const pubkeys: Map = new Map(); 188 | for (let i in items) { 189 | pubkeys.set(items[i]['_id'], items[i]['pubKey']); 190 | } 191 | return pubkeys; 192 | }) 193 | ); 194 | } 195 | 196 | public getHearts(user: User) { 197 | return this.http.get<{ time: number, votes: ReceivedHeart[] }>('/api/hearts/get/' + (+user.data.lastCheck) + '/' + user._id) 198 | .pipe( 199 | map((hearts) => { 200 | const nuser = { 201 | ...user 202 | }; 203 | nuser.data.lastCheck = hearts.time; 204 | const userhearts = nuser.data.received; 205 | for(let vote of hearts.votes) { 206 | const attempt = user.crypto.decryptAsym(vote.v); 207 | if (!attempt) { 208 | continue; 209 | } 210 | userhearts.push(vote); 211 | } 212 | nuser.data.received = userhearts; 213 | return nuser; 214 | }), 215 | tap((nuser) => this.user$.next(nuser)), 216 | tap(() => this.save()) 217 | ); 218 | } 219 | 220 | public submit() { 221 | return this.getPubKeys().pipe( 222 | switchMap((pub) => this.declareChoices(pub)), 223 | tap(() => this.save()), 224 | tap(() => { 225 | this.user$.next({ 226 | ...this.user$.value, 227 | submitted: true, 228 | }); 229 | }) 230 | ); 231 | } 232 | 233 | public login(username: string, _password: string) { 234 | const password = Crypto.hash(Crypto.hash(Crypto.hash(_password))); 235 | return this.http.post('/api/session/login', { username, password }).pipe( 236 | switchMap(() => this.getInfo(username, _password)), 237 | switchMap((user) => this.getHearts(user)), 238 | catchError((err) => { 239 | console.log(err); 240 | if (err.error instanceof Error) { 241 | // A client-side or network error occurred. Handle it accordingly. 242 | } else { 243 | // The backend returned an unsuccessful response code. 244 | // The response body may contain clues as to what went wrong, 245 | if (err.status === 202) { 246 | return of(''); 247 | } else if (err.status === 403) { 248 | return Observable.throw('It seems you entered a wrong password'); 249 | } else if (err.status === 404) { 250 | return Observable.throw('Your roll number is wrong. Please use your IITK roll no.'); 251 | } 252 | } 253 | return Observable.throw('An Error Occurred!! Please try again.'); 254 | }) 255 | ); 256 | } 257 | 258 | public signup(body: any) { 259 | return this.http.post('/api/users/login/first', body).pipe( 260 | catchError((err) => Observable.throw('Wrong Authentication Code!! Try Again')) 261 | ); 262 | } 263 | 264 | public mail(roll: string): Observable { 265 | return this.http.get('/api/users/mail/' + roll).pipe( 266 | map(() => 'Mail sent to your @iitk ID !'), 267 | catchError((err) => { 268 | if (err.error instanceof Error) { 269 | // A client-side or network error occurred. Handle it accordingly. 270 | } else { 271 | // The backend returned an unsuccessful response code. 272 | // The response body may contain clues as to what went wrong, 273 | if (err.status === 202) { 274 | return of('Mail sent to your @iitk ID !'); 275 | } else if (err.status === 404) { 276 | return of('Your information was not found in our database. Please send us a mail at pclubiitk@gmail.com'); 277 | } else if (err.status === 400) { 278 | return of('You have already registered'); 279 | } 280 | } 281 | return of('There was an error. Let us know at pclubiitk@gmail.com'); 282 | }) 283 | ); 284 | } 285 | 286 | public matches(): Observable { 287 | const user = this.user$.value; 288 | return this.http.get('/api/users/data/match/' + user._id).pipe( 289 | tap(x => this.matches$.next(x)), 290 | catchError((err) => { 291 | console.error(err); 292 | return Observable.throw('There was an error. Please try again.'); 293 | }) 294 | ); 295 | } 296 | 297 | public stats(): Observable { 298 | return this.http.get('/api/stats').pipe( 299 | catchError((err) => { 300 | console.error(err); 301 | return Observable.throw('There was an error. Please try again.'); 302 | }), 303 | ); 304 | } 305 | 306 | save(): Promise { 307 | const user = this.user$.value; 308 | const data = user.data; 309 | console.log(data); 310 | const encData = user.crypto.encryptSym(Crypto.fromJson(data)); 311 | return this.http.post('/api/users/data/update/' + user._id, { 312 | data: encData, 313 | }).toPromise(); 314 | } 315 | 316 | } 317 | -------------------------------------------------------------------------------- /controllers/user.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/pclubiitk/puppy-love/db" 11 | "github.com/pclubiitk/puppy-love/models" 12 | "github.com/pclubiitk/puppy-love/utils" 13 | 14 | "github.com/gin-gonic/gin" 15 | "gopkg.in/mgo.v2/bson" 16 | ) 17 | 18 | var Db db.PuppyDb 19 | 20 | func UserDelete(c *gin.Context) { 21 | id, err := SessionId(c) 22 | if err != nil || id != "admin" { 23 | c.AbortWithStatus(http.StatusForbidden) 24 | return 25 | } 26 | 27 | if err := Db.GetCollection("user").DropCollection(); err != nil { 28 | c.String(http.StatusInternalServerError, 29 | "Could not delete collection") 30 | return 31 | } 32 | 33 | c.String(http.StatusOK, "Deleted user table") 34 | } 35 | 36 | func UserNew(c *gin.Context) { 37 | id, err := SessionId(c) 38 | if err != nil || id != "admin" { 39 | c.AbortWithStatus(http.StatusForbidden) 40 | log.Print("Unauthorized creation attempt by: " + id) 41 | log.Print(err) 42 | return 43 | } 44 | 45 | info := new(models.TypeUserNew) 46 | if err := c.BindJSON(info); err != nil { 47 | c.AbortWithStatus(http.StatusBadRequest) 48 | return 49 | } 50 | 51 | user := models.NewUser(info) 52 | 53 | if err := Db.GetCollection("user").Insert(&user); err != nil { 54 | c.AbortWithStatus(http.StatusInternalServerError) 55 | log.Print(err) 56 | return 57 | } 58 | 59 | c.JSON(http.StatusAccepted, "Information set up") 60 | } 61 | 62 | // User's first login 63 | // ------------------ 64 | func UserFirst(c *gin.Context) { 65 | info := new(models.TypeUserFirst) 66 | if err := c.BindJSON(info); err != nil { 67 | c.AbortWithStatus(http.StatusBadRequest) 68 | return 69 | } 70 | 71 | user := models.User{} 72 | 73 | // Fetch user 74 | if err := Db.GetById("user", info.Id).One(&user); err != nil { 75 | c.AbortWithStatus(http.StatusNotFound) 76 | log.Print(err) 77 | return 78 | } 79 | 80 | // If auth code did not match 81 | if user.AuthC != info.AuthCode || user.AuthC == "" { 82 | c.AbortWithStatus(http.StatusForbidden) 83 | return 84 | } 85 | 86 | // Edit information 87 | if _, err := Db.GetById("user", info.Id). 88 | Apply(user.FirstLogin(info), &user); err != nil { 89 | 90 | c.AbortWithStatus(http.StatusInternalServerError) 91 | log.Print(err) 92 | return 93 | } 94 | 95 | // Remove user's auth token 96 | if _, err := Db.GetById("user", info.Id). 97 | Apply(user.SetField("autoCode", ""), &user); err != nil { 98 | 99 | c.AbortWithStatus(http.StatusInternalServerError) 100 | log.Print(err) 101 | return 102 | } 103 | 104 | c.JSON(http.StatusAccepted, "Information set up") 105 | } 106 | 107 | // User asking for email 108 | // --------------------- 109 | func UserMail(c *gin.Context) { 110 | id := c.Param("id") 111 | 112 | type mailData struct { 113 | Email string `json:"email" bson:"email"` 114 | AuthC string `json:"authCode" bson:"authCode"` 115 | } 116 | 117 | u := mailData{} 118 | 119 | if err := Db.GetById("user", id).One(&u); err != nil { 120 | c.AbortWithStatus(http.StatusNotFound) 121 | log.Print(err) 122 | return 123 | } 124 | 125 | if u.AuthC == "" { 126 | c.String(http.StatusBadRequest, "You have already signed up") 127 | return 128 | } 129 | 130 | // Queue this request in service 131 | err := utils.SignupRequest(id) 132 | if err != nil { 133 | c.String(http.StatusInternalServerError, "Something went wrong") 134 | } 135 | 136 | c.JSON(http.StatusAccepted, 137 | fmt.Sprintf("Mail will be sent to %s", u.Email)) 138 | } 139 | 140 | func MatchGet(c *gin.Context) { 141 | id, err := SessionId(c) 142 | if err != nil || c.Param("you") != id { 143 | c.AbortWithStatus(http.StatusForbidden) 144 | log.Println("Failed on match get: " + id) 145 | log.Println(err) 146 | return 147 | } 148 | 149 | type typeUserGet struct { 150 | ID string `json:"_id" bson:"_id"` 151 | Matches string `json:"matches" bson:"matches"` 152 | } 153 | 154 | user := new(typeUserGet) 155 | 156 | // Fetch user 157 | if err := Db.GetById("user", id).One(user); err != nil { 158 | c.AbortWithStatus(http.StatusNotFound) 159 | log.Print(err) 160 | return 161 | } 162 | 163 | c.JSON(http.StatusOK, (*user)) 164 | } 165 | 166 | // Get user's information 167 | // ---------------------- 168 | type typeUserGet struct { 169 | Id string `json:"_id" bson:"_id"` 170 | Name string `json:"name" bson:"name"` 171 | Gender string `json:"gender" bson:"gender"` 172 | Image string `json:"image" bson:"image"` 173 | PubK string `json:"pubKey" bson:"pubKey"` 174 | } 175 | 176 | func UserGet(c *gin.Context) { 177 | id := c.Param("id") 178 | 179 | user := models.User{} 180 | 181 | // Fetch user 182 | if err := Db.GetById("user", id).One(&user); err != nil { 183 | c.AbortWithStatus(http.StatusNotFound) 184 | log.Print(err) 185 | return 186 | } 187 | 188 | resp := typeUserGet{ 189 | Id: user.Id, 190 | Name: user.Name, 191 | Gender: user.Gender, 192 | Image: user.Image, 193 | PubK: user.PubK, 194 | } 195 | 196 | c.JSON(http.StatusAccepted, resp) 197 | } 198 | 199 | // @AUTH Get user's private information on login 200 | // --------------------------------------- 201 | 202 | type typeUserLoginGet struct { 203 | Id string `json:"_id" bson:"_id"` 204 | Name string `json:"name" bson:"name"` 205 | Gender string `json:"gender" bson:"gender"` 206 | Image string `json:"image" bson:"image"` 207 | PrivK string `json:"privKey" bson:"privKey"` 208 | PubK string `json:"pubKey" bson:"pubKey"` 209 | Data string `json:"data" bson:"data"` 210 | Submit bool `json:"submitted" bson:"submitted"` 211 | Matches string `json:"matches" bson:"matches"` 212 | Email string `json:"email" bson:"email"` 213 | } 214 | 215 | func UserLoginGet(c *gin.Context) { 216 | id, err := SessionId(c) 217 | if err != nil { 218 | c.AbortWithStatus(http.StatusForbidden) 219 | log.Println("Failed on login info: " + id) 220 | log.Println(err) 221 | return 222 | } 223 | 224 | user := models.User{} 225 | 226 | // Fetch user 227 | if err := Db.GetById("user", id).One(&user); err != nil { 228 | c.AbortWithStatus(http.StatusNotFound) 229 | log.Print(err) 230 | return 231 | } 232 | 233 | resp := typeUserLoginGet{ 234 | Id: user.Id, 235 | Name: user.Name, 236 | Email: user.Email, 237 | Gender: user.Gender, 238 | Image: user.Image, 239 | PrivK: user.PrivK, 240 | PubK: user.PubK, 241 | Data: user.Data, 242 | Submit: user.Submit, 243 | Matches: user.Matches, 244 | } 245 | 246 | c.JSON(http.StatusAccepted, resp) 247 | } 248 | 249 | // After user submits all choices 250 | // ------------------------------ 251 | 252 | func UserSubmitTrue(c *gin.Context) { 253 | id, err := SessionId(c) 254 | if err != nil || id != c.Param("you") { 255 | c.AbortWithStatus(http.StatusForbidden) 256 | return 257 | } 258 | 259 | user := models.User{} 260 | if err := Db.GetById("user", id).One(&user); err != nil { 261 | c.JSON(http.StatusNotFound, "Invalid user") 262 | log.Print(err) 263 | return 264 | } 265 | 266 | heartsAndChoices := new(models.HeartsAndChoices) 267 | if err := c.BindJSON(heartsAndChoices); err != nil { 268 | c.String(http.StatusBadRequest, "Invalid JSON") 269 | log.Print(err) 270 | return 271 | } 272 | 273 | // First, send the hearts using sendHearts 274 | if err = sendHearts(user, heartsAndChoices.Hearts); err != nil { 275 | c.JSON(http.StatusBadRequest, "Failed, probably the request is invalid") 276 | log.Print(err) 277 | return 278 | } 279 | 280 | // Then, declare the choices 281 | if err = declareStep(user, heartsAndChoices.Tokens); err != nil { 282 | c.JSON(http.StatusBadRequest, "Failed, probably the request is invalid") 283 | log.Print(err) 284 | return 285 | } 286 | 287 | if _, err := Db.GetById("user", id). 288 | Apply(user.SetField("submitted", true), &user); err != nil { 289 | 290 | c.AbortWithStatus(http.StatusInternalServerError) 291 | log.Print(err) 292 | return 293 | } 294 | 295 | } 296 | 297 | func declareStep(user models.User, info models.Declare) error { 298 | 299 | if info.Id != user.Id { 300 | return errors.New("Invalid session/userId") 301 | } 302 | 303 | // TODO: fix db name to not be a constant 304 | if _, err := Db.GetCollection("declare").UpsertId(user.Id, bson.M{ 305 | "t0": info.Token0, 306 | "t1": info.Token1, 307 | "t2": info.Token2, 308 | "t3": info.Token3, 309 | }); err != nil { 310 | return err 311 | } 312 | return nil 313 | } 314 | 315 | func difference(oldVotes []models.Heart, 316 | newVotes []models.GotHeart) []models.GotHeart { 317 | 318 | diff := []models.GotHeart{} 319 | m := map[string]int{} 320 | for _, s1val := range oldVotes { 321 | m[s1val.Data] = 1 322 | } 323 | 324 | for _, s2val := range newVotes { 325 | if m[s2val.Data] != 1 { 326 | diff = append(diff, s2val) 327 | } 328 | } 329 | 330 | return diff 331 | } 332 | 333 | // Serve when a Heart is to be saved 334 | func sendHearts(user models.User, info []models.GotHeart) error { 335 | // Check that user isn't voting more than 4 336 | // ======================================== 337 | 338 | userVotes := new([]models.Heart) 339 | if err := Db.GetCollection("heart"). 340 | Find(bson.M{"roll": user.Id}). 341 | All(userVotes); err != nil { 342 | return err 343 | } 344 | 345 | diffHearts := difference(*userVotes, info) 346 | 347 | log.Print("Earlier count: ", len(*userVotes)) 348 | log.Print("Sent new: ", len(diffHearts)) 349 | 350 | if len(diffHearts)+len(*userVotes) > 4 { 351 | return errors.New("More than allowed votes") 352 | } 353 | 354 | ctime := uint64(time.Now().UnixNano() / 1000000) 355 | 356 | newHearts := []models.Heart{} 357 | for _, heart := range diffHearts { 358 | newHearts = append(newHearts, 359 | models.Heart{ 360 | Id: user.Id, 361 | Gender: heart.GenderOfSender, 362 | Time: ctime, 363 | Value: heart.Value, 364 | Data: heart.Data, 365 | }) 366 | } 367 | 368 | bulk := Db.GetCollection("heart").Bulk() 369 | for _, heart := range newHearts { 370 | bulk.Insert(heart) 371 | } 372 | 373 | _, err := bulk.Run() 374 | 375 | if err != nil { 376 | return err 377 | } 378 | return nil 379 | } 380 | 381 | // @AUTH Update user data 382 | // ------------------------------ 383 | 384 | func UserUpdateData(c *gin.Context) { 385 | id, err := SessionId(c) 386 | if err != nil || id != c.Param("you") { 387 | c.AbortWithStatus(http.StatusForbidden) 388 | return 389 | } 390 | 391 | type typeUserUpdateData struct { 392 | Data string `json:"data"` 393 | } 394 | 395 | info := new(typeUserUpdateData) 396 | if err := c.BindJSON(info); err != nil { 397 | c.AbortWithStatus(http.StatusBadRequest) 398 | return 399 | } 400 | 401 | user := models.User{} 402 | 403 | if _, err := Db.GetById("user", id). 404 | Apply(user.SetField("data", info.Data), &user); err != nil { 405 | 406 | c.AbortWithStatus(http.StatusInternalServerError) 407 | log.Print(err) 408 | return 409 | } 410 | 411 | c.JSON(http.StatusAccepted, "Saved successfully") 412 | } 413 | 414 | // @AUTH Update user image 415 | // ------------------------------ 416 | 417 | func UserUpdateImage(c *gin.Context) { 418 | id, err := SessionId(c) 419 | if err != nil || id != c.Param("you") { 420 | c.AbortWithStatus(http.StatusForbidden) 421 | return 422 | } 423 | 424 | type imgstruct struct { 425 | Image string `json:"img" bson:"img"` 426 | } 427 | 428 | user := models.User{} 429 | info := new(imgstruct) 430 | 431 | if err := c.BindJSON(info); err != nil { 432 | c.AbortWithStatus(http.StatusBadRequest) 433 | return 434 | } 435 | 436 | if _, err := Db.GetById("user", id). 437 | Apply(user.SetField("image", info.Image), &user); err != nil { 438 | 439 | c.AbortWithStatus(http.StatusInternalServerError) 440 | log.Print(err) 441 | return 442 | } 443 | 444 | c.JSON(http.StatusAccepted, "Saved successfully") 445 | } 446 | 447 | // @AUTH Update user passsave 448 | // ------------------------------ 449 | func UserSavePass(c *gin.Context) { 450 | id, err := SessionId(c) 451 | if err != nil || id != c.Param("you") { 452 | c.AbortWithStatus(http.StatusForbidden) 453 | return 454 | } 455 | 456 | type imgstruct struct { 457 | Pass string `json:"pass" bson:"pass"` 458 | } 459 | 460 | user := models.User{} 461 | info := new(imgstruct) 462 | 463 | if err := c.BindJSON(info); err != nil { 464 | c.AbortWithStatus(http.StatusBadRequest) 465 | return 466 | } 467 | 468 | if _, err := Db.GetById("user", id). 469 | Apply(user.SetField("savepass", info.Pass), &user); err != nil { 470 | 471 | c.AbortWithStatus(http.StatusInternalServerError) 472 | log.Print(err) 473 | return 474 | } 475 | 476 | c.JSON(http.StatusAccepted, "Saved successfully") 477 | } 478 | -------------------------------------------------------------------------------- /report/puppylove.tex: -------------------------------------------------------------------------------- 1 | % 2 | % LaTeX template for prepartion of submissions to SIGTBD'16 3 | % 4 | % Requires temporary version of sigplanconf style file provided on 5 | % SIGTBD'16 web site. 6 | % 7 | \documentclass[sigtbd]{sigtbd-style} 8 | % \documentclass[SIGTBD-cameraready]{sigplanconf-SIGTBD16} 9 | % 10 | % the following standard packages may be helpful, but are not required 11 | % 12 | \usepackage{courier} % standard fixed width font 13 | \usepackage[scaled]{helvet} % see www.ctan.org/get/macros/latex/required/psnfss/psnfss2e.pdf 14 | \usepackage{url} % format URLs 15 | \usepackage{listings} % format code 16 | \usepackage{epigraph} 17 | \usepackage{enumitem} % adjust spacing in enums 18 | \usepackage[colorlinks=true,allcolors=blue,breaklinks,draft=false]{hyperref} % hyperlinks, including DOIs and URLs in bibliography 19 | % known bug: http://tex.stackexchange.com/questions/1522/pdfendlink-ended-up-in-different-nesting-level-than-pdfstartlink 20 | \newcommand{\doi}[1]{doi:~\href{http://dx.doi.org/#1}{\Hurl{#1}}} % print a hyperlinked DOI 21 | 22 | % Comments 23 | \newcommand{\va}[1]{\textcolor{red}{{\em VA:} #1}} 24 | \newcommand{\sss}[1]{\textcolor{red}{{\em SS:} #1}} 25 | \newcommand{\mv}[1]{\textcolor{red}{{\em MV:} #1}} 26 | 27 | \newcommand{\name}{\textit{Puppy Love}} 28 | 29 | \begin{document} 30 | 31 | \title{\name{}: Cryptographically secure anonymous couple matching} 32 | 33 | % 34 | % any author declaration will be ignored when using 'SIGTBD' option (for double blind review) 35 | % 36 | 37 | \authorinfo{Saksham Sharma} 38 | {\makebox{Computer Science and Technology} \\ 39 | \makebox{Indian Institute of Technology Kanpur} \\ 40 | {sakshams@cse.iitk.ac.in}} 41 | 42 | \maketitle 43 | \begin{abstract} 44 | Anonymous and secure dating algorithms are not only hard to define, 45 | but apparently tough to realize as well. Tinder et all have 46 | succeeded in making this a mainstream application, and yet, there is 47 | scope left in improving the anonymity of such platforms to provide 48 | a guarantee to the users that their choices shall not be made 49 | public, nor known to the administrators. 50 | 51 | We designed an algorithm for this problem which provides strong 52 | guarantees about privacy of users, and implemented it inside our 53 | campus, catering to almost 2000 users. The algorithm is designed to 54 | be easily scalable, and was a success in the university setting. 55 | \end{abstract} 56 | 57 | \section{Introduction} 58 | 59 | The queerly named \title{} platform has been running in IIT Kanpur 60 | since 2014, meant to help shy nerds meet their crush. The platform 61 | opens one week prior to the Valentine's Day every year, and lets people 62 | choose up to 4 of their crushes. At the stroke of the midnight hour on 63 | 14th February, people who happen to like each other are informed about 64 | the same. If your \textit{love} was unrequited (the other person 65 | didn't like you), you will not get to know. More importantly, if you 66 | did not like the other person, you would not know if that person liked 67 | you or not. In addition, people can see the count of people who have 68 | selected their names without finding out who they are. 69 | 70 | Since a campus is often a close knit circle, users are apprehensive 71 | about revealing their crushes to others, including the administrators 72 | (who are students themselves), lest it leak out. That makes it 73 | necessary to design a technique to ensure that users are guaranteed 74 | that obtaining their choices is either impossible or computationally 75 | infeasible for anyone but them. 76 | 77 | We developed a new version of the platform which provides strong 78 | privacy guarantees without compromising on performance or features. 79 | 80 | \section{Requirements from the algorithm} 81 | The following discussion refers to $Alice$ and $Bob$. All 82 | arguments apply to each of them.\\ 83 | The security goals are the following: 84 | \begin{enumerate} 85 | \item $Alice$ should not find out the choice of person $Bob$ if $Alice$ 86 | did not like $Bob$. 87 | \item It should not be possible for $Alice$ to cheat and obtain 88 | whether $Alice$ matched with the $Bob$, without this 89 | information being made available for $Bob$. In other words, the 90 | algorithm should be fair, and ideally symmetric. 91 | \item The administrator should not be able to obtain the choices of 92 | a person, or brute force the data in any way to deduce the likes 93 | of a person. 94 | \item The administrator should not find out which people matched, but 95 | knowing the count of matches is reasonable. 96 | \item The user should be able to verify that the frontend is not 97 | sending any such information which could reveal their 98 | choices. Thus, the privacy guarantees should hold whatever code is 99 | running on the backend. 100 | \item The counting technique should function independently without 101 | revealing the identities of the users. 102 | \item Security features should be practical to implement in a web 103 | browser and should be completely transparent to the user. 104 | \item It can be assumed that the server does not collude with 105 | another involved party. Thus, the server is assumed to be 106 | honest-but-curious and will not disrupt the service intentionally. 107 | \end{enumerate} 108 | Some other practical requirements: 109 | \begin{enumerate} 110 | \item The algorithm should have no false positives or false negatives. 111 | \item It should ideally involve only $O(1)$ computation by each 112 | involved party. 113 | \item There should ideally be no to-and-fro communication between the 114 | involved parties. 115 | \end{enumerate} 116 | 117 | \section{Related work} 118 | A similar problem was proposed and tackled in \cite{Senpai:1} where 119 | the proposed various algorithms, including one which does present a 120 | secure protocol. The drawback of such an algorithm is that it requires 121 | all involved parties to take part in $O(n)$ computation, where $n$ is 122 | the number of other parties. 123 | 124 | Our algorithm is completely different from this work, but it is 125 | interesting to look at the mentioned paper nonetheless. 126 | 127 | \bibliographystyle{abbrvnat} 128 | 129 | \section{Initial approaches} 130 | We went through a lot of approaches for this problem, before finally 131 | arriving at the presented approach, which, although extremely simple, 132 | is not obvious at first. 133 | 134 | We initially modelled the problem as computing the AND of $O(n^2)$ bits, 135 | disregarding the requirement of $O(1)$ computations per person. 136 | 137 | \subsection{Homomorphic computation} 138 | 139 | Our initial attempts at homomorphic were foiled by the fact that 140 | completely homomorphic systems are impractical. 141 | 142 | In contrast, partially homomorphic functions, although practical, do 143 | not provide any function which can be used for this purpose. The 144 | available computations are XOR, multiply, addition over 1 bit et 145 | all. Composing any two of these functions provides enough information 146 | to both parties to violate privacy concerns. 147 | 148 | \subsection{Yao's two party computation} 149 | Yao presented a secure protocol to compute any arbitrary function 150 | (which can be run using logic gates) across two parties in a shared 151 | manner. The protocol was not originally intended to be fair though, 152 | and one party could cheat in some respect. 153 | 154 | We describe the protocol here: 155 | \begin{itemize} 156 | \item Alice party creates a truth table for an AND gate, with columns 157 | A (her choice), B (Bob's choice) and C (result). 158 | \item She selects 6 random values $a_0, a_1, b_0, b_1, c_0, c_1$. She 159 | then shuffles the table, and encrypts $((A = i) \& (B = j)) = Ck$ 160 | with the value $a_i-b_j$. She then shares this table with Bob. 161 | \item If Alice's boolean choice is $choice$, she sends $a_{choice}$ to 162 | Bob as well. 163 | \item Bob obtains $b_{bob's choice}$ from Alice using Oblivious 164 | transfer, such that Alice will not know which bit value was obtained 165 | by Bob. 166 | \item Bob tries decrypting the table with the key $a_{alice}-b_{bob}$ 167 | and finds out what value he obtained. He can then look up whether 168 | they matched or not. 169 | \end{itemize} 170 | 171 | We attempted to make the protocol fair by using the central server, 172 | who would then match both parties and declare their result to them. 173 | 174 | A clever hack around this was found on noticing the asymmetry in the 175 | algorithm. Alice could forge the truth table to return a $mismatch$ if 176 | Bob likes Alice, and a $match$ otherwise. A false positive can be 177 | borne easily, but if Bob did like Alice, he would not know that Alice 178 | has cheated and has found out about his liking. This is certainly 179 | unacceptable. 180 | 181 | \subsection{Public key encrypted messages} 182 | This involves the following step: 183 | \begin{itemize} 184 | \item All parties get to send 4 values to the server. 185 | \item If Alice likes Bob, she will send a message (string containing 186 | their roll numbers) encrypted with Bob's 187 | public key to the server. 188 | \item Bob will encrypt the same message with his own public key and 189 | store it on the server. 190 | \item The person to use the other's public key is chosen using 191 | lexicographic comparison. 192 | \item The server will detect duplicates and declare matches. 193 | \end{itemize} 194 | 195 | This is a very simple algorithm, but fails on a computationally 196 | feasible brute force attack. The server can easily try out all 197 | possible messages which Alice could have sent, and stop when it 198 | produces a value which is the same as the one sent by Alice. 199 | 200 | \subsection{An O(n) algorithm using duplicates} 201 | We observed that instead of trying to compute an AND, it may be 202 | interesting to instead use the duplicate detection in another way. 203 | 204 | An algorithm for this is quite simple: 205 | \begin{itemize} 206 | \item Both parties establish an encrypted channel using their public 207 | keys. 208 | \item They both exchange a random token over this channel. Let the 209 | tokens be $A, B$. 210 | \item Both of them declare a value $hash(A+B)$ to the server, who 211 | verifies that the values are the same. 212 | \item Each party gets to send 4 new values to the server. 213 | \item If Alice likes Bob, she will send the pre-decided $A$ to the 214 | server. Otherwise she will send a random value. 215 | \item Server computes $hash(A_{received}-B_{received})$ and matches it 216 | with the values received earlier. If they are the same, it declares 217 | a match. 218 | \end{itemize} 219 | 220 | Although this algorithm has no obvious security concerns, it involves 221 | a lot of computation, infact $O(n)$ computation per person. Our tests 222 | showed a huge performance bottleneck on modern browsers while using 223 | Stanford's $sjcl$ library for crypto-protocol implementations. 224 | 225 | The platform originally launched with this algorithm, but the 226 | performance issues on scaling to $O(1000)$ people made it practically 227 | impossible to run. 228 | 229 | \section{The Puppy Love algorithm} 230 | The final algorithm was inspired by the previous $O(n)$ algorithm, 231 | wherein it was identified that it is essential to come up with a 232 | message which only the involved parties can create, and no one 233 | else. This is the only step which was causing a $O(n)$ bottleneck. 234 | 235 | Apart from that, some other requirements (counting the number of 236 | people who like you, stopping the server from knowing the matched 237 | people) required some additions to this algorithm which are described 238 | later. 239 | 240 | \subsection{Matching} 241 | \begin{itemize} 242 | \item There is a known generator field $g$. 243 | \item Let the public key of Alice be $g^a$ and her private key be 244 | $a$. The same is true for Bob's $g^b$ and $b$. 245 | \item The value $g^{ab}$ cannot be computed by anyone other than Alice 246 | and Bob, since it is the Discrete Logarithm problem, which is known 247 | to not lie in PTime. 248 | \item Both parties get to send 4 values to the server. 249 | \item If Alice likes Bob, she will send $g^{ab}$ to 250 | the server. The same applies to Bob. 251 | \item The server declares a match if it detects a duplicate value. 252 | \end{itemize} 253 | 254 | Some remarks: 255 | \begin{enumerate} 256 | \item The algorithm itself is quite simple, and does not present any 257 | apparent privacy concerns. The only issue (to be resolved later) is 258 | that the server will know which two people have matched. 259 | \item It is extremely fast to implement, and running times are ideal 260 | for browsers, since it involves only 4 maximum mathematical 261 | computations. 262 | \item Values received by the server appear completely random to it, 263 | and cannot be brute forced. 264 | \item The value of $g^{ab}$ is very easily obtained if the 265 | Public-Private keypairs are ElGamal, as was in our case. This value 266 | is simply the Diffie Hellman value computed in Elliptic Curve 267 | Cryptography, and Stanford's $sjcl$ library provides a direct 268 | function for this computation. 269 | \end{enumerate} 270 | 271 | \subsection{Implementing the counting of people who like you} 272 | We opted for a very simple solution to this, albeit it has a minor scope 273 | for inconsistencies. 274 | 275 | When Alice sends her preferences, her client also encrypts a 276 | random string with the public key of Bob (if Bob is among her 277 | likes). This value is stored on the server in a list ordered by 278 | timestamps. Other clients retrieve all new entries in this list 279 | everytime they log back in, and try decrypting all received 280 | values. The client can detect that some of the messages were encrypted 281 | with the user's public key, but cannot detect the sender of the 282 | message. 283 | 284 | \subsection{Ensuring that the server does not know the matches} 285 | This was not implemented during 2017's deployment of the platform, but 286 | should work fine regardless. 287 | \begin{itemize} 288 | \item Before sending the final chosen values to the server, the client 289 | obtains the blind signatures of the values it plans to send. 290 | \item The blind signatures, as well as the actual values which are 291 | being sent will not be known to the server. 292 | \item The client now switches IP (in our case, sends the packet 293 | through the NAT of the university, re-entering the university 294 | network using a reverse proxy, thus completely anonymizing the 295 | identity of the sender), and sends its 4 tokens (with 296 | signature) to the server without attaching its login cookie with 297 | them. The server can only verify that the received values are 298 | correctly signed. 299 | \item The server declares all the matched tokens in the end, and 300 | clients can check if their token is among this list of matched 301 | tokens. 302 | \end{itemize} 303 | 304 | \subsection{Miscellaneous security concerns} 305 | There are some other concerns which are discussed here: 306 | \begin{itemize} 307 | \item IP switching is unreliable. The user may not be expected to 308 | undertake this arduous task of accessing the platform behind a 309 | VPN/Tor for the step of sending the tokens. Thankfully, our 310 | university setup had a network which is known to be not in the 311 | control of the students, and thus the packets may use the NAT to 312 | ensure their identities are safe. 313 | \item It is possible for the server to display fake public keys for 314 | users. This surely is a concern, but can be easily verified. The 315 | server does not ask for cookies while serving public keys, and thus, 316 | it can be easily verified (by a concerned user) whether a different 317 | public key is being server to some people. If the university 318 | infrastructure allowed for reliable hosting of public keys, this 319 | task could be eased. 320 | \item The server may serve fake code for some users which reveals more 321 | information. But in such a case, the hashes of the code received and 322 | the code expected will not match, and a concerned user can verify 323 | the same, or run a local copy of the code (which she has manually 324 | verified to be safe). 325 | \end{itemize} 326 | 327 | \section{Deployment} 328 | The platform was well received, with more than 1800 students 329 | registering on the website, and as many as 45 matched couples. There 330 | were many people who had multiple matches, both among boys and girls. 331 | 332 | Although girls are outnumbered on the campus $1:10$, among the 333 | registrants, the ratio was close to $1:5$. 334 | 335 | The backend was implemented in Golang, using the $iris$ web 336 | framework. The frontend was written using Angular2 and TypeScript, 337 | communicating with the backend using REST API of the backend. The 338 | server resource usage was monitored and was observed to be very low 339 | even during peak times, due to Golang's lightweight web 340 | frameworks. MongoDB was used for data storage, and Redis was used to 341 | ensure persistent sessions. 342 | 343 | \begin{thebibliography}{} 344 | \softraggedright 345 | \bibitem[(2015)]{Senpai:1} 346 | Senpai: Solving the dating problem 347 | \newblock SIGTBD 2017. 348 | 349 | \end{thebibliography} 350 | 351 | \end{document} 352 | 353 | %%% Local Variables: 354 | %%% mode: latex 355 | %%% TeX-master: t 356 | %%% End: 357 | --------------------------------------------------------------------------------