601 lines
19 KiB
Go
601 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"github.com/go-chi/chi/v5"
|
|
"net/url"
|
|
"os"
|
|
"time"
|
|
|
|
"crypto/ed25519"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"html/template"
|
|
"io/fs"
|
|
"net/http"
|
|
"path/filepath"
|
|
|
|
library "git.ailur.dev/ailur/fg-library/v2"
|
|
authLibrary "git.ailur.dev/ailur/fg-nucleus-library"
|
|
"git.oreonproject.org/oreonproject/eternity/common"
|
|
"git.oreonproject.org/oreonproject/eternity/lib"
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
var ServiceInformation = library.Service{
|
|
Name: "eternity",
|
|
Permissions: library.Permissions{
|
|
Authenticate: true, // This service does require authentication
|
|
Database: true, // This service does require database access
|
|
BlobStorage: false, // This service does not require blob storage
|
|
InterServiceCommunication: true, // This service does require inter-service communication
|
|
Resources: true, // This service does require its HTTP templates and static files
|
|
},
|
|
ServiceID: uuid.MustParse("0f31fa2d-43ca-410f-8148-239b298112b3"),
|
|
}
|
|
|
|
func logFunc(message string, messageType uint64, information library.ServiceInitializationInformation) {
|
|
// Log the message to the logger service
|
|
information.Outbox <- library.InterServiceMessage{
|
|
ServiceID: ServiceInformation.ServiceID,
|
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000002"), // Logger service
|
|
MessageType: messageType,
|
|
SentAt: time.Now(),
|
|
Message: message,
|
|
}
|
|
}
|
|
|
|
func renderTemplate(statusCode int, w http.ResponseWriter, data map[string]interface{}, templatePath string, information library.ServiceInitializationInformation) {
|
|
var err error
|
|
var requestedTemplate *template.Template
|
|
// Output ls of the resource directory
|
|
requestedTemplate, err = template.ParseFS(information.ResourceDir, "templates/"+templatePath)
|
|
if err != nil {
|
|
logFunc(err.Error(), 2, information)
|
|
renderString(500, w, "Sorry, something went wrong on our end. Error code: 01. Please report to the administrator.", information)
|
|
} else {
|
|
w.WriteHeader(statusCode)
|
|
err = requestedTemplate.Execute(w, data)
|
|
if err != nil {
|
|
logFunc(err.Error(), 2, information)
|
|
renderString(500, w, "Sorry, something went wrong on our end. Error code: 02. Please report to the administrator.", information)
|
|
}
|
|
}
|
|
}
|
|
|
|
func renderString(statusCode int, w http.ResponseWriter, data string, information library.ServiceInitializationInformation) {
|
|
w.WriteHeader(statusCode)
|
|
_, err := w.Write([]byte(data))
|
|
if err != nil {
|
|
logFunc(err.Error(), 2, information)
|
|
}
|
|
}
|
|
|
|
func renderJSON(statusCode int, w http.ResponseWriter, data map[string]interface{}, information library.ServiceInitializationInformation) {
|
|
w.WriteHeader(statusCode)
|
|
err := json.NewEncoder(w).Encode(data)
|
|
if err != nil {
|
|
logFunc(err.Error(), 2, information)
|
|
}
|
|
}
|
|
|
|
func verifySecret(secret string, conn library.Database) bool {
|
|
// Check if the secret is in the secrets table
|
|
var secretCheck string
|
|
err := conn.DB.QueryRow("SELECT secret FROM secrets WHERE secret = $1", secret).Scan(&secretCheck)
|
|
if err != nil || secretCheck != secret {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func verifyJwt(token string, publicKey ed25519.PublicKey, conn library.Database) (jwt.MapClaims, bool) {
|
|
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
|
return publicKey, nil
|
|
})
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
if !parsedToken.Valid {
|
|
return nil, false
|
|
}
|
|
|
|
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
// Check if the token expired
|
|
date, err := claims.GetExpirationTime()
|
|
if err != nil || date.Before(time.Now()) || claims["sub"] != nil || claims["isOpenID"] != nil || claims["isOAuth"].(bool) {
|
|
return claims, false
|
|
}
|
|
|
|
// Check if the token is in users
|
|
var idCheck []byte
|
|
err = conn.DB.QueryRow("SELECT id FROM users WHERE id = $1", claims["sub"]).Scan(&idCheck)
|
|
if err != nil || claims["sub"] != uuid.Must(uuid.FromBytes(idCheck)).String() {
|
|
return claims, false
|
|
}
|
|
|
|
return claims, true
|
|
}
|
|
|
|
func Main(information library.ServiceInitializationInformation) {
|
|
var conn library.Database
|
|
gitDir := information.Configuration["gitDir"].(string)
|
|
outputDir := information.Configuration["outputDir"].(string)
|
|
hostName := information.Configuration["hostName"].(string)
|
|
|
|
// Initiate a connection to the database
|
|
// Call service ID 1 to get the database connection information
|
|
information.Outbox <- library.InterServiceMessage{
|
|
ServiceID: ServiceInformation.ServiceID,
|
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Service initialization service
|
|
MessageType: 1, // Request connection information
|
|
SentAt: time.Now(),
|
|
Message: nil,
|
|
}
|
|
|
|
// Wait for the response
|
|
response := <-information.Inbox
|
|
if response.MessageType == 2 {
|
|
// This is the connection information
|
|
// Set up the database connection
|
|
conn = response.Message.(library.Database)
|
|
if conn.DBType == library.Sqlite {
|
|
// Create the packages table
|
|
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS packages (creator BLOB NOT NULL, name TEXT NOT NULL, path TEXT NOT NULL)")
|
|
if err != nil {
|
|
logFunc(err.Error(), 3, information)
|
|
}
|
|
// Create the secrets table
|
|
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS secrets (secret BLOB NOT NULL)")
|
|
if err != nil {
|
|
logFunc(err.Error(), 3, information)
|
|
}
|
|
// Create the users table
|
|
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB NOT NULL)")
|
|
if err != nil {
|
|
logFunc(err.Error(), 3, information)
|
|
}
|
|
} else {
|
|
// Create the packages table
|
|
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS packages (creator BYTEA NOT NULL, name TEXT NOT NULL, path TEXT NOT NULL)")
|
|
if err != nil {
|
|
logFunc(err.Error(), 3, information)
|
|
}
|
|
// Create the secrets table
|
|
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS secrets (secret BYTEA NOT NULL)")
|
|
if err != nil {
|
|
logFunc(err.Error(), 3, information)
|
|
}
|
|
// Create the users table
|
|
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA NOT NULL)")
|
|
if err != nil {
|
|
logFunc(err.Error(), 3, information)
|
|
}
|
|
}
|
|
} else {
|
|
// This is an error message
|
|
// Log the error message to the logger service
|
|
logFunc(response.Message.(error).Error(), 3, information)
|
|
}
|
|
|
|
// Ask the authentication service for the public key
|
|
information.Outbox <- library.InterServiceMessage{
|
|
ServiceID: ServiceInformation.ServiceID,
|
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service
|
|
MessageType: 2, // Request public key
|
|
SentAt: time.Now(),
|
|
Message: nil,
|
|
}
|
|
|
|
var publicKey ed25519.PublicKey = nil
|
|
|
|
// 3 second timeout
|
|
go func() {
|
|
time.Sleep(3 * time.Second)
|
|
if publicKey == nil {
|
|
logFunc("Timeout while waiting for the public key from the authentication service", 3, information)
|
|
}
|
|
}()
|
|
|
|
// Wait for the response
|
|
response = <-information.Inbox
|
|
if response.MessageType == 2 {
|
|
// This is the public key
|
|
publicKey = response.Message.(ed25519.PublicKey)
|
|
} else {
|
|
// This is an error message
|
|
// Log the error message to the logger service
|
|
logFunc(response.Message.(error).Error(), 3, information)
|
|
}
|
|
|
|
// Ask the authentication service to create a new OAuth2 client
|
|
urlPath, err := url.JoinPath(hostName, "/oauth")
|
|
if err != nil {
|
|
logFunc(err.Error(), 3, information)
|
|
}
|
|
|
|
information.Outbox <- library.InterServiceMessage{
|
|
ServiceID: ServiceInformation.ServiceID,
|
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service
|
|
MessageType: 1, // Create OAuth2 client
|
|
SentAt: time.Now(),
|
|
Message: authLibrary.OAuthInformation{
|
|
Name: "Eternity Web",
|
|
RedirectUri: urlPath,
|
|
KeyShareUri: "",
|
|
Scopes: []string{"openid"},
|
|
},
|
|
}
|
|
|
|
oauthResponse := authLibrary.OAuthResponse{}
|
|
|
|
// 3 second timeout
|
|
go func() {
|
|
time.Sleep(3 * time.Second)
|
|
if oauthResponse == (authLibrary.OAuthResponse{}) {
|
|
logFunc("Timeout while waiting for the OAuth response from the authentication service", 3, information)
|
|
}
|
|
}()
|
|
|
|
// Wait for the response
|
|
response = <-information.Inbox
|
|
switch response.MessageType {
|
|
case 0:
|
|
// Success, set the OAuth response
|
|
oauthResponse = response.Message.(authLibrary.OAuthResponse)
|
|
logFunc("Initialized with App ID: "+oauthResponse.AppID, 0, information)
|
|
case 1:
|
|
// An error which is their fault
|
|
logFunc(response.Message.(error).Error(), 3, information)
|
|
case 2:
|
|
// An error which is our fault
|
|
logFunc(response.Message.(error).Error(), 3, information)
|
|
default:
|
|
// An unknown error
|
|
logFunc("Unknown error", 3, information)
|
|
}
|
|
|
|
// Set up the router
|
|
router := chi.NewRouter()
|
|
|
|
// Set up the static routes
|
|
staticDir, err := fs.Sub(information.ResourceDir, "static")
|
|
if err != nil {
|
|
logFunc(err.Error(), 3, information)
|
|
} else {
|
|
router.Handle("/static-eternity/*", http.StripPrefix("/static-eternity/", http.FileServerFS(staticDir)))
|
|
}
|
|
|
|
// Set up the API routes
|
|
router.Post("/api/packages/list", func(w http.ResponseWriter, r *http.Request) {
|
|
// Get the list of packages
|
|
rows, err := conn.DB.Query("SELECT name FROM packages")
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "01"}, information)
|
|
return
|
|
}
|
|
|
|
var packages []string
|
|
for rows.Next() {
|
|
var name string
|
|
err = rows.Scan(&name)
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "02"}, information)
|
|
return
|
|
}
|
|
packages = append(packages, name)
|
|
}
|
|
|
|
renderJSON(200, w, map[string]interface{}{
|
|
"packages": packages,
|
|
}, information)
|
|
})
|
|
|
|
router.Post("/api/packages/add", func(w http.ResponseWriter, r *http.Request) {
|
|
type packageData struct {
|
|
Name string `json:"name"`
|
|
RepositoryPath string `json:"repositoryPath"`
|
|
JwtToken string `json:"token"`
|
|
Secret string `json:"secret"`
|
|
}
|
|
var data packageData
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
|
|
return
|
|
}
|
|
|
|
var claims jwt.MapClaims
|
|
if data.Secret != "" {
|
|
// Verify the secret
|
|
if !verifySecret(data.Secret, conn) {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid secret"}, information)
|
|
return
|
|
}
|
|
} else {
|
|
// Verify the JWT token
|
|
var ok bool
|
|
claims, ok = verifyJwt(data.JwtToken, publicKey, conn)
|
|
if !ok {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Fetch the Git repository
|
|
_, err = git.PlainClone(filepath.Join(gitDir, data.Name), false, &git.CloneOptions{
|
|
Depth: 1,
|
|
URL: data.RepositoryPath,
|
|
})
|
|
if err != nil {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid repository path"}, information)
|
|
return
|
|
}
|
|
|
|
// Add the package to the database
|
|
var userid []byte
|
|
if data.Secret != "" {
|
|
userid, err = uuid.MustParse("00000000-0000-0000-0000-000000000000").MarshalBinary()
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information)
|
|
}
|
|
} else {
|
|
userid, err = uuid.MustParse(claims["sub"].(string)).MarshalBinary()
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information)
|
|
}
|
|
}
|
|
|
|
_, err = conn.DB.Exec("INSERT INTO packages (creator, name, path) VALUES ($1, $2, $3)", userid, data.Name, data.RepositoryPath)
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information)
|
|
return
|
|
}
|
|
|
|
renderJSON(200, w, map[string]interface{}{"success": true}, information)
|
|
})
|
|
|
|
router.Post("/api/packages/remove", func(w http.ResponseWriter, r *http.Request) {
|
|
type packageData struct {
|
|
Name string `json:"name"`
|
|
JwtToken string `json:"token"`
|
|
Secret string `json:"secret"`
|
|
}
|
|
var data packageData
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
|
|
return
|
|
}
|
|
|
|
// Verify the JWT token
|
|
var claims jwt.MapClaims
|
|
if data.Secret != "" {
|
|
// Verify the secret
|
|
if !verifySecret(data.Secret, conn) {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid secret"}, information)
|
|
return
|
|
}
|
|
} else {
|
|
// Verify the JWT token
|
|
var ok bool
|
|
claims, ok = verifyJwt(data.JwtToken, publicKey, conn)
|
|
if !ok {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Remove the package from the database
|
|
var userid []byte
|
|
if data.Secret != "" {
|
|
userid, err = uuid.MustParse("00000000-0000-0000-0000-000000000000").MarshalBinary()
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information)
|
|
}
|
|
} else {
|
|
userid, err = uuid.MustParse(claims["sub"].(string)).MarshalBinary()
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information)
|
|
}
|
|
}
|
|
|
|
_, err = conn.DB.Exec("DELETE FROM packages WHERE creator = $1 AND name = $2", userid, data.Name)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
renderJSON(404, w, map[string]interface{}{"error": "Package not found"}, information)
|
|
return
|
|
} else {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Remove the package from the filesystem
|
|
err = os.RemoveAll(filepath.Join(gitDir, data.Name))
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "05"}, information)
|
|
return
|
|
}
|
|
|
|
renderJSON(200, w, map[string]interface{}{"success": true}, information)
|
|
})
|
|
|
|
router.Post("/api/packages/get", func(w http.ResponseWriter, r *http.Request) {
|
|
type packageData struct {
|
|
Name string `json:"name"`
|
|
JwtToken string `json:"token"`
|
|
Secret string `json:"secret"`
|
|
}
|
|
var data packageData
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
|
|
return
|
|
}
|
|
|
|
// Verify the JWT token
|
|
if data.Secret != "" {
|
|
// Verify the secret
|
|
if !verifySecret(data.Secret, conn) {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid secret"}, information)
|
|
return
|
|
}
|
|
} else {
|
|
// Verify the JWT token
|
|
var ok bool
|
|
_, ok = verifyJwt(data.JwtToken, publicKey, conn)
|
|
if !ok {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information)
|
|
return
|
|
}
|
|
}
|
|
|
|
var nameCheck string
|
|
err = conn.DB.QueryRow("SELECT name FROM packages WHERE name = $1", data.Name).Scan(&nameCheck)
|
|
if err != nil || nameCheck != data.Name {
|
|
if err != nil && errors.Is(err, sql.ErrNoRows) {
|
|
renderJSON(404, w, map[string]interface{}{"error": "Package not found"}, information)
|
|
return
|
|
} else {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Get the package from the resource directory
|
|
packageEpk, err := os.ReadFile(filepath.Join(outputDir, "packages", data.Name+".epk"))
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "06"}, information)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
w.WriteHeader(200)
|
|
_, err = w.Write(packageEpk)
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "07"}, information)
|
|
logFunc(err.Error(), 2, information)
|
|
}
|
|
})
|
|
|
|
router.Post("/api/packages/compile", func(w http.ResponseWriter, r *http.Request) {
|
|
type packageData struct {
|
|
Name string `json:"name"`
|
|
JwtToken string `json:"token"`
|
|
Secret string `json:"secret"`
|
|
PrivateKey string `json:"privateKey"`
|
|
}
|
|
var data packageData
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
if err != nil {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
|
|
return
|
|
}
|
|
|
|
// Verify the JWT token
|
|
if data.Secret != "" {
|
|
// Verify the secret
|
|
if !verifySecret(data.Secret, conn) {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid secret"}, information)
|
|
return
|
|
}
|
|
} else {
|
|
// Verify the JWT token
|
|
var ok bool
|
|
_, ok = verifyJwt(data.JwtToken, publicKey, conn)
|
|
if !ok {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid JWT token"}, information)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Fetch the package from the database
|
|
var nameCheck, path string
|
|
err = conn.DB.QueryRow("SELECT name, path FROM packages WHERE name = $1", data.Name).Scan(&nameCheck, &path)
|
|
if err != nil || nameCheck != data.Name {
|
|
if err != nil && errors.Is(err, sql.ErrNoRows) {
|
|
renderJSON(404, w, map[string]interface{}{"error": "Package not found"}, information)
|
|
return
|
|
} else {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Decode the base64 private key
|
|
privateKeyBytes, err := base64.StdEncoding.DecodeString(data.PrivateKey)
|
|
if err != nil {
|
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid private key"}, information)
|
|
return
|
|
}
|
|
|
|
// Compile the package
|
|
// I'm sure you all know how eternity works, so I'm not going to explain this
|
|
// If you don't, https://git.oreonproject.org/oreonproject/eternity, it's really cool!
|
|
err = os.Chdir(filepath.Join(gitDir, data.Name))
|
|
if err != nil {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "08"}, information)
|
|
return
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
logger := &lib.Logger{
|
|
LogFunc: func(log lib.Log) string {
|
|
stdout.Write([]byte(log.Content))
|
|
if log.Level == "fatal" {
|
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "09", "stdout": stdout.String()}, information)
|
|
}
|
|
return ""
|
|
},
|
|
PromptSupported: false,
|
|
StdoutSupported: true,
|
|
Stdout: &stdout,
|
|
}
|
|
|
|
config, err, vagueErr := lib.ParseConfig("eternity.json", logger)
|
|
if err != nil || vagueErr != nil {
|
|
common.EternityJsonHandleError(err, vagueErr, logger)
|
|
}
|
|
|
|
size, tempDir, err, vagueErr := lib.BuildEPK("./", false, config.Build, logger)
|
|
if err != nil || vagueErr != nil {
|
|
common.BuildEPKHandleError(tempDir, err, vagueErr, logger)
|
|
}
|
|
|
|
config.Metadata.DecompressedSize = size
|
|
|
|
err, vagueErr = lib.PackageEPK(config.Metadata, config.Build, tempDir, filepath.Join(outputDir, "packages", data.Name+".epk"), privateKeyBytes, logger)
|
|
if err != nil || vagueErr != nil {
|
|
common.PackageEPKHandleError(err, vagueErr, logger)
|
|
}
|
|
|
|
common.BuildEPKRemoveTemporaryDirectory(tempDir, logger)
|
|
renderJSON(200, w, map[string]interface{}{"success": true, "stdout": stdout.String()}, information)
|
|
})
|
|
|
|
// Set up the template routes
|
|
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
renderTemplate(200, w, map[string]interface{}{}, "index.html", information)
|
|
})
|
|
|
|
router.Get("/packages", func(w http.ResponseWriter, r *http.Request) {
|
|
renderTemplate(200, w, map[string]interface{}{}, "packages.html", information)
|
|
})
|
|
|
|
router.Get("/oauth", func(w http.ResponseWriter, r *http.Request) {
|
|
renderTemplate(200, w, map[string]interface{}{
|
|
"ClientId": oauthResponse.AppID,
|
|
}, "oauth.html", information)
|
|
})
|
|
}
|